Fusioncube

The online journey of a technophile, by Steve Brownlee

The ExtJS framework has a GridPanel object with the ability to group items into sections defined by the developer.  It uses a GroupingStore, and a GroupingView in order to visually group items into a loose tree-type structure.

The biggest benefit to that mechanism is that the framework handles all the sorting, DOM elements and interaction code to make it possible.

However, if you’re not using a GridPanel (it’s a heavy component) and just want to display your data in a series of panels, there is no automatic way to group your data, so you need to roll your own code.

Luckily, the framework provides plenty of helper methods to make this a fairly trivial task. All I needed to do was extend the Ext.Panel class and, on initialization, use the collect() method on the assigned Store to determine the groups, and then use the query() method to get all records assigned to each of the groups.

See this code in action using HBox and VBox layouts.

GroupedPanel Extension

Ext.ux.GroupedPanel = Ext.extend(Ext.Panel, {
   itemConfig: {
      tpl: new Ext.XTemplate(''),
      selector: '',
      margins: ''
   },
   dataConfig: {
      store: new Ext.data.Store({}),
      records: null,
      groupColumn: ''
   },

   initComponent : function(){
      var group_records,i=0,group_panels=[], groups;

      // Initialize superclass
      Ext.ux.GroupedPanel.superclass.initComponent.call(this);

      // If a data structure is specified, load it into the store
      if (this.dataConfig.records != null) this.dataConfig.store.load(this.dataConfig.records);

      // Use the collect() method to get the unique values in the specified group column
      groups = this.dataConfig.store.collect(this.dataConfig.groupColumn);

      // Loop through each group
      for (; i < groups.length; i=i+1){

         // Use the query() method to get the records in the current group
         group_records = this.dataConfig.store.query(this.dataConfig.groupColumn, groups[i]);

         // Create a panel to contain the current group's records
         // Use the specified XTemplate and itemSelector
         group_panels[group_panels.length] = new Ext.Panel({
            frame:    true,
            flex:     1,
            margins:  this.itemConfig.margins,
            title:    groups[i],
            items: new Ext.DataView({
               store:        group_records,
               tpl:          this.itemConfig.tpl,
               autoHeight:   true,
               itemSelector: this.itemConfig.selector
            })
         });
      }

      // Add all of the group panels to this
      this.add(group_panels);
   }
});

GroupedPanel Usage Example

// This panel will contain all group panels, using the vbox layout to
// stack them vertically, and also stretch each panel to its width.
var ContainerPanel = new Ext.ux.GroupedPanel({
   layout: 'vbox',
   layoutConfig: {
      align: 'stretch',
      pack: 'start'
   },
   height:600,
   dataConfig: {
      store: TeammateStore,
      groupColumn: 'Department',
   },
   itemConfig: {
      tpl: new Ext.XTemplate(
         '<tpl for=".">',
         '<div class="teammate" id="{tmID}">',
            '<div class="thumb">{tmID} - {Teammate}</div>',
         '</div>',
         '</tpl>'
         {
            compiled: true
         }
      ),
      selector: 'div.teammate',
      margins: '10 0 0 0'
   }
});

ContainerPanel.render('group-grid');
Published on Monday, Jan 3,2011 | 0 Comments

I can’t believe it took this long for this to click in my head. I’m sure I had seen the bubbleEvents() config option on ExtJS component documentation before, but my eyes just swept over it like when you’re looking for your kid in a crowd. the unimportant information immediately gets dismissed by your brain.

Well, today, I noticed an old bit of code in a Ext.Window object.  Here’s an example:

MappingDialog = function(){
   var dialog;
   return {
      init : function(){
         dialog = new Ext.Window({
            width:     200,
            height:    150,
            modal:     true,
            closable:  false,
            buttons: [
               { text:'Save', handler:function(){ dialog.fireEvent('saveMapping'); } },
               { text:'Cancel', handler:function(){ dialog.hide() } }
            ]
         });

         dialog.addListener('saveMapping', this.saveData, this);
      },
      saveData : function(){
         ... Save the data ...
      },
      showWindow : function(){ dialog.show(); },
      hideWindow : function(){ dialog.hide(); }
   }
};

Notice how I’m referencing the dialog object inside the Save and Cancel button handlers. That’s ugly, but at the time I wrote it I didn’t know how to do it differently. Today, however, I focused in on that dialog.fireEvent() statement and thought to myself, “Self, there has to be a way to bubble the event of a button up to its parent window, so I can avoid having direct references to the Window inside the Button – which is inside the Window.”

Off to the ExtJS API Docs and I immediately notice the bubbleEvents config option which, like I said, I’d glazed over hundreds of times in the past. remember? Well, using that, I now bubble any event up to its parent container which can handle it appropriately. Much more efficient code from a memory perspective and from a maintenance perspective as well, as renaming the Window won’t require a global search/replace.

MappingDialog = function(){
   return {
      init : function(){
         dialog = new Ext.Window({
            width:     200,
            height:    150,
            modal:     true,
            closable:  false,
            <strong>bubbleEvents: ['saveMapping'],</strong>
            buttons: [{
               text:'Save',
               <strong>bubbleEvents: ['saveMapping'],</strong>
               handler:function(){ this.fireEvent('saveMapping'); }
            },{
               text:'Cancel',
               <strong>bubbleEvents: ['closeWindow'],</strong>
               handler:function(){ this.fireEvent('closeWindow'); }
            ],
            listeners: {
               saveMapping : function() { this.fireEvent('saveMapping'); },
               closeWindow : function() { this.close(); }
            }
         });

         this.addListener('saveMapping', this.saveData, this);
      },
      saveData : function(){
         ... Save the data ...
      },
      showWindow : function(){ dialog.show(); },
      hideWindow : function(){ dialog.hide(); }
   }
};
Published on Wednesday, Dec 22,2010 | 2 Comments

Pretty straightforward example of expanding and collapsing all grouped rows by clicking on the Checkbox item.

See this code in action

// Sample JSON data for teammates, their department
// and compensation rate
var Teammates = [{
    tmID: 1, Teammate:'Steve', Department:'Safety', Compensation_Rate:'30', Date_Change:'01/01/1999'},{
    tmID: 2, Teammate:'Steve', Department:'IT', Compensation_Rate:'55', Date_Change:'05/07/2002'},{
    tmID: 3, Teammate:'John', Department:'Sales', Compensation_Rate:'50', Date_Change:'03/05/2000'},{
    tmID: 4, Teammate:'John', Department:'Executive', Compensation_Rate:'53', Date_Change:'05/10/2000'},{
    tmID: 5, Teammate:'Matthew', Department:'Welding', Compensation_Rate:'55', Date_Change:'12/17/2002'
}];

// Create a Checkbox that will toggle between
// expanding and collapsing the grid items
ExpandCollapseAll = new Ext.form.Checkbox({
    renderTo: Ext.getBody(),
    boxLabel: 'Expand Results',
    listeners: {
        check: function(ctrl, val) {
            if (val) {
                RateGrid.view.expandAllGroups();
            } else {
                RateGrid.view.collapseAllGroups();
            }
        }
    }
});

// A GridPanel using a GroupingView to display the data
// grouped by the teammate name
RateGrid = new Ext.grid.GridPanel({
   renderTo: Ext.getBody(),
   width:    400,
   style: 'margin: 10px 10px 10px 10px',
   height:   300,
   store:    new Ext.data.GroupingStore({
               data:   Teammates,
               reader: new Ext.data.JsonReader({ id: 'tmID' },
                  Ext.data.Record.create([
                     {name: 'tmID', type: 'string'},
                     {name: 'Teammate', type: 'string'},
                     {name: 'Department', type: 'string'},
                     {name: 'Compensation_Rate', type: 'string'},
                     {name: 'Date_Change', type: 'string'}
                  ])),
               groupField: 'Teammate',
               sortInfo: {field:'Date_Change', direction: 'ASC'}
   }),
   cm:       new Ext.grid.ColumnModel([
               {
                  id: 'Teammate',
                  dataIndex: 'Teammate',
                  hidden: true
               },{
                  id: 'Department',
                  header: 'Department',
                  dataIndex: 'Department',
                  width: 150
               },{
                  id: 'Compensation',
                  header: 'Rate',
                  dataIndex: 'Compensation_Rate',
                  width: 100
               },{
                  id: 'DateChange',
                  header: 'Date',
                  dataIndex: 'Date_Change',
                  width: 100
               }
   ]),
   autoScroll:  true,
   autoExpandColumn: 'Department',
   view:        new Ext.grid.GroupingView({
                  showGroupName: false,
                  hideGroupedColumn: true,
                  startCollapsed: true,
                  groupTextTpl:  '{text}'
   })
});
Published on Monday, Dec 20,2010 | 0 Comments

I wanted to finally get rid of any usage of the JavaScript eval() function when parsing JSON returned from my components.

I went to JSON.org and noticed that there’s an updated json2.js file that should replace my old json.js file as well as two separate parsing engine files on the GitHub site:

  • json_parse.js
  • json_parse_state.js

Once again, I customized.  I removed the existing JSON.parse() method from json2.js and replaced it with the function on json_parse_state.js, so all I need to do is

<script type="text/javascript" src="js/JSON.js"></script>

and the JSON.parse() method will now use the state engine code instead of eval().

Download custom JSON.js.

Published on Tuesday, Dec 7,2010 | 0 Comments

I’ve been using the AjaxCFC library for years. It’s my preferred way of integrating Javascript and ColdFusion via AJAX. I’ve even modified it from its original form so that my implementation was strictly for integration with jQuery, only returns JSON strings (ignoring WDDX and simple string), and can work with ColdSpring.

Now that I’m a heavy user of the Sencha ExtJS framework, I thought it would be useful to port the jQuery.ajaxCFC.js file over to an Ext.AjaxCFC.js file that extended the native Ext.data.Connection class and utilized the Ext.Ajax object.

Took me about half the day, but I finally got a working Ext.AjaxCFC.request() method that uses the same syntax as the $.AjaxCFC() method. For those familiar with the inner workings and code of the jQuery AjaxCFC class, this will look very familiar.

So now I can make AJAX calls using native ExtJS classes, access ColdSpring beans in my application’s bean factory, or connect directly to any CFC

Ext.AjaxCFC.js

Ext.AjaxCFCConnection = Ext.extend(Ext.data.Connection, {
   data        : null,
   queryFormat : 'array',
   factory     : (typeof(__ajaxConfig) == 'undefined') ? null : __ajaxConfig.beanFactory,
   timeout     : (typeof(__ajaxConfig) == 'undefined') ? 30000 : __ajaxConfig.defaultTimeout,
   url         : __ajaxConfig.url,
   bean        : null,

   request : function(arguments) {
      var params = (typeof(arguments.data) == 'undefined') ? {} : arguments.data;
      arguments.params = {};
      arguments.params['C0-ID']         = (Math.floor(Math.random() * 10001) + "_" + new Date().getTime()).toString(),
      arguments.params['method']        = 'init';
      arguments.params['component']     = arguments.component;
      arguments.params['bean']          = (typeof(arguments.bean) == 'undefined') ? this.bean : arguments.bean;
      arguments.params['factory']       = this.factory;
      arguments.params['C0-METHODNAME'] = arguments.method;
      arguments.params['queryFormat']   = (typeof(arguments.queryFormat) == 'undefined') ? this.queryFormat : arguments.queryFormat;
      arguments.params['C0-PARAM0']     = params;

      arguments.url = this.url + '?method=' + arguments.params['method'];
      arguments.method = 'POST';
      arguments.failure = arguments.error;
      arguments.timeout = this.timeout;

      var ____success = arguments.success;

      arguments.success = function(data) {
         data = data.responseText.replace(/^\s*|\s*$/g, '');

         if (data.substring(0,9) == '__json__:') {
            data = Ext.util.JSON.decode(data.slice(9));
         }
         ____success(data, this);
      };

      if ( params ) {
         if (typeof params != 'string') {
            arguments.params['C0-PARAM0'] = Ext.util.JSON.encode(params);
         }
      }

      Ext.Ajax.request(arguments);
   }
});

Ext.AjaxCFC = new Ext.AjaxCFCConnection();

Usage

// Include Ext.AjaxCFC code
<script type="text/javascript" src="js/Ext.AjaxCFC.js"></script>

// Default configuration properties for the ajaxCFC library
__ajaxConfig = {
   'url':'/myApp/ajaxCFC/ajax.cfc',
   'defaultTimeout':30000,
   'beanFactory':'application.beanFactory'
};

// AjaxCFC call using Ext.data.Connection class
Ext.AjaxCFC.request({
   bean: 'AColdSpringBean',
   method: 'aMethod',
   data: {
      'id': 416198,
      'first_name': 'Steve',
      'last_name': 'Brownlee'
   },
   success: function(details, s){
      DataStore.loadData(details);
   },
   error: function(results){
      Ext.MessageBox.alert('Search Failed', 'An unexpected error occurred. Please try again.');
   }
});
Published on Tuesday, Dec 7,2010 | 1 Comment

About Steve

I am a technologist, and have been ever since 1980 when I got my very first TRS-80 and programmed it to do my math homework. I love to share the gift of technology with others and show them the wonderful things it can do for them, and how they should not fear it, but embrace it.

Latest Tweets

  • This is an awesome idea, that would likely be heavily abused..."Connecting Web Apps with Web Intents" http://t.co/QAU4DNcF
  • It is exciting working in a startup in a booming, emerging industry.
  • Absolutely loving the new changes to the Scripts app in Chrome dev tools. Thanks @ChromeBrowser team!
  • AR Will Turn Your Living Room into Mario’s Next Level http://t.co/8gZvmKpu

Subscribe

Entries (RSS)
Comments (RSS)