Weird bug preventing ExtJS checkboxes from submitting properly

This applies to ExtJS 2.2, the most current version as of the time of writing.

Checkboxes often make their way into my Ext JS forms. Sometimes, though, they don’t behave as expected. Checking and unchecking them would frequently fail, simply not doing anything. Sometimes it would work, sometimes it wouldn’t – how frustrating!

It turns out there is a bug with ticking/unticking checkboxes in Ext. If you click on the checkbox itself everything works fine – the image of the checkbox updates and the correct value is submitted. If however you click on the checkbox’s label, the image of the checkbox is updated but the correct value is not submitted. So if the box started off unticked and you ticked it by clicking the label, the image is updated but nothing else happens.

This is extremely unintuitive because you can see that the box has been checked, but its internal representation hasn’t actually changed. Because I usually click the label this took me over an hour to track down, so I hope this helps someone out. Once I had identified the bug, a quick Google search points to this thread on the ExtJS forums, which has some guidance on this.

Cleaning up an example Ext JS form

One of my recent Ext JS forms had a section which looked like this:

items: [
  new Ext.Button({
    text: 'Preview Video',
    iconCls: 'play',
    handler: function() {
      var win;
      
      if (!win) {
        win = new Ext.Window({
          title: 'Preview Video',
          modal: true,
          height: 377,
          width: 368,
          items: [
            new Ext.Panel({
              autoLoad: '/admin/videos/' + video_id + '/preview.html'
            })
          ],
          buttons: [
            {
              text: 'OK',
              handler: function() {
                win.close();
              }
            }
          ]
        });
        
      };
      win.show();
      
    }
  })
]

Not horrific but not nice either – let’s DRY this up. It’s not too pleasant to read but all it’s really doing is rendering a customised Ext.Button which opens up a modal Ext.Window, in which is loaded the contents of a known url.

Ok so let’s start with that Window. First, we’ll make a subclass of Ext.Window:

/**
 * AdFunded.views.Video.PreviewWindow
 * @extends Ext.Window
 * A simple Preview window for the given video_id
 */
AdFunded.views.Video.PreviewWindow = function(config) {
  var config = config || {};
    
  Ext.applyIf(config, {
    title: 'Preview Video',
    modal: true,
    height: 377,
    width: 368,
    items: [
      new Ext.Panel({
        autoLoad: '/admin/videos/' + config.video_id + '/preview.html'
      })
    ],
    buttons: [
      {
        text: 'OK',
        scope: this,
        handler: function() {
          this.window.close();
        }
      }
    ]
  });
  
  AdFunded.views.Video.PreviewWindow.superclass.constructor.call(this, config);
  
  this.window = this;
};
Ext.extend(AdFunded.views.Video.PreviewWindow, Ext.Window);
Ext.reg('video_preview_window', AdFunded.views.Video.PreviewWindow);

Note the namespacing employed above – within an Ext MVC framework I have been developing across several projects for the last few months, all views follow this structure. AdFunded is the name of the application. The precise structure doesn’t matter here, but using a namespace for each app does.

So we’ve taken the Window setup out of our view now, which leaves us with:

items: [
  new Ext.Button({
    text: 'Preview Video',
    iconCls: 'play',
    handler: function() {
      var win;
      
      if (!win) {
        win = new AdFunded.views.Video.PreviewWindow({video_id: id});
      };
      win.show();
      
    }
  })
]

Great – we’ve gone from 34 lines in our view to 15, and scored ourselves a reusable Window component which we can call from anywhere in the app. Nice work, but there’s more to come… If we’re going to use the Preview Window again, we’ll probably need to use that Preview Button again too. Let’s see:

/**
 * AdFunded.views.Video.PreviewButton
 * @extends Ext.Button
 * Displays a Preview Window for the given video_id
 */
AdFunded.views.Video.PreviewButton = function(config) {
  var config = config || {};
  
  Ext.applyIf(config, {
    text: 'Preview Video',
    iconCls: 'play',
    handler: function() {
      var win = new AdFunded.views.Video.PreviewWindow({video_id: config.video_id});
      win.show();
    }
  });
  
  AdFunded.views.Video.PreviewButton.superclass.constructor.call(this, config);
};
Ext.extend(AdFunded.views.Video.PreviewButton, Ext.Button);
Ext.reg('video_preview_button', AdFunded.views.Video.PreviewButton);

Which leaves us with the following the the view:

items: [
  {
    xtype: 'video_preview_button',
    video_id: id
  }
]

We’ve now gone from 34 lines to 6 (in the view at least), but the point is not about cutting out lines of code – it’s creating reusable components. We’ve added 20 lines overall this way but we now have two extra components that we can call on at any time (with minimal lines of code), safe in the knowledge that they will provide a consistent experience each time.

ExtJS Radio Buttons and Square Brackets

While creating an ExtJS form with several radio buttons today I ran into a bug which caused none of them to work as expected, even though there were no errors/exceptions. To cut a long story short, it was because I was setting the name to “schedule[include_type]” – like this:

{
  xtype: 'radio',
  name: 'schedule[include_type]',
  inputValue: 'page',
  boxLabel: 'Show page:'
}

This radio button is one of 4, which allows the user which type of file they want to include on a particular model (a Schedule in this case) – be it Page, Video, Category or one other. The thing is – none of them work with the square brackets in the name. If you remove the brackets, they all work correctly, but the server-side is relying on those brackets to be present to group the data correctly.

In the end I bit the bullet and updated my submit method to add a new parameter directly – here’s a full example:

form = new Ext.form.FormPanel({
  items: [
    {
      xtype: 'radio',
      name: 'include_type',
      inputValue: 'page',
      boxLabel: 'Show page:'
    },
    {
      xtype: 'radio',
      name: 'include_type',
      inputValue: 'category',
      boxLabel: 'Show category:'
    },
    ... plus some extra items
  ],
  buttons: [
    {
      text: 'Save',
      handler: function() {
        
        //find the currently selected include_type from the form
        var include_type = this.form.getValues()['include_type'];
        
        //note the params option - this needs to be added manually otherwhise 
        //schedule[include_type] won't appear
        form.form.submit({
          waitMsg: 'Saving Data...',
          params: "schedule[include_type]=" + include_type,
          url: some url...
        });
      }
    }
  ]
})

Note: I don’t usually add buttons in the way above so I’m not sure if the form.form.submit will work correctly here – see http://extjs.com/deploy/dev/docs/?class=Ext.form.FormPanel for information about overriding submit.

So what we’re doing here is finding which radio button is currently checked, and appending this under “schedule[include_type]” when POSTing the form variables to the server. This really isn’t pleasant but seems to be the best way around this limitation for now.

I regularly use square brackets in other Ext JS Fields – Radio Buttons seem to be the only ones that have this problem. http://extjs.com/forum/showthread.php?p=185296 has a bit of background behind this, but no real solution.

Useful Rails javascript expansions for EXTJS

If you’re using Edge Rails (or > 2.1, which isn’t out at time of writing), and are using the EXT JS framework anywhere, here are a couple of handy javascript include tag expansions to clean up your views. Just chuck them into any file in your config/initializers directory:

ActionView::Helpers::AssetTagHelper.register_javascript_expansion :ext => ['ext/adapter/ext/ext-base', 'ext/ext-all']

ActionView::Helpers::AssetTagHelper.register_javascript_expansion :ext_grid_filter => ['ext/ux/menu/EditableItem', 'ext/ux/menu/RangeMenu', 'ext/ux/grid/GridFilters', 'ext/ux/grid/filter/Filter', 'ext/ux/grid/filter/StringFilter', 'ext/ux/grid/filter/DateFilter', 'ext/ux/grid/filter/ListFilter', 'ext/ux/grid/filter/NumericFilter', 'ext/ux/grid/filter/BooleanFilter']

The top one includes the relevant EXT base files and the second one includes all the Grid Filters from the excellent Filter Grid plugin (see http://ccinct.com/lab/filter-grid/.

Include them as usual like this:

javascript_include_tag :ext, :ext_grid_filter, :cache => 'ext_javascripts'

EXT remote-loading forms with Combo boxes

Something that’s harder than it should be is populating an EXT edit form with form data, where one of the form fields is a select box. If there is a specific set of values that this select can take, then you can hard code that using a SimpleStore, like this:

var exampledata = [['AL', 'Alabama'],
                   ['AK', 'Alaska'],
                   ['AZ', 'Arizona'],
                   ['AR', 'Arkansas'],
                   ['CA', 'California']];

var store = new Ext.data.SimpleStore({
    fields: ['abbr', 'state'],
    data : exampleData
});

var combo = new Ext.form.ComboBox({
    store: store,
    displayField: 'state',
    valueField: 'abbr',
    ... etc ...
});

form = new Ext.form({
  items: [combo],
  ... etc ...
});

form.load(url_to_load_from);

So that will populate the select box with the static values you’ve defined (the 5 states above), then when the form loads it will select the appropriate option automatically.

So far so good, but what if you need to load what goes into the select box dynamically? Well, first you’ll need to set up your remote data store (my server is sending back JSON data, hence the JsonReader):

store = new Ext.data.Store({
  url: 'url_to_load_combo_data_from',
  reader: new Ext.data.JsonReader(
    {root: 'states',totalProperty: 'results'},
    [
      {name: 'name', type: 'string', mapping: 'state.name'},
      {name: 'abbr', type: 'string', mapping: 'state.abbr'}
    ]
  )
});

This will consume data like this:

{
  "states": [
    {"state": {"name": "Alabama", "abbr": "AL"}}, 
    {"state": {"name": "Alaska",  "abbr": "AK"}}
  ], 
  "results": "2"
}

And populate the store with a collection of state records which can then be loaded into the combobox.

Then all you need to do is load the store before loading the form data, and your comboboxes will be correctly populated, displaying the correct option. Here’s the full example:

store = new Ext.data.Store({
  url: 'url_to_load_combo_data_from',
  reader: new Ext.data.JsonReader(
    {root: 'states',totalProperty: 'results'},
    [
      {name: 'name', type: 'string', mapping: 'state.name'},
      {name: 'abbr', type: 'string', mapping: 'state.abbr'}
    ]
  )
});

var combo = new Ext.form.ComboBox({
    store: store,
    displayField: 'state',
    valueField: 'abbr'
    ... etc ...
});

form = new Ext.formPanel({
  items: [combo]
});

store.load();
form.load(your_form_data_url);

Be wary using the pagination options on the combobox here (see http://extjs.com/deploy/dev/docs/?class=Ext.form.ComboBox) – the reason being if your state’s ‘abbr’ features on the second page of the results it won’t populate the correct options into the combo box.

Getting EXT PagingToolbars to save state

A problem that has recently had me pulling my hair out is how to save state in an EXT PagingToolbar.

Ext makes it easy to save the state of most of its components – by default it does this by setting a cookie with the relevant configuration info, then just reading it back when you load the component again. I’ve been using it to save the state of a few EXT grids I’ve been using on a recent project, this saves config such as which columns you have visible, which column you’re sorting by, and how the columns are ordered.

That works great, and is trivial to implement – just set your provider (see http://extjs.com/deploy/dev/docs/?class=Ext.state.CookieProvider) and be sure to give your grid an id in its config – this is used as the key in the state provider and needs to be unique for each component.

The problem comes when you’re using a paging toolbar though, as this does not save state, so every time you view the grid you’re back to page 1. You can add state behaviour to the paginator by piggybacking the grid’s state store, here’s how it’s done:

Ext.PagingToolbar.override({
  init : function (grid) {
    this.grid = grid;        
    this.grid.on("beforestatesave", this.saveState, this);    
    Ext.util.Observable.capture(grid.store, this.onStateChange, this);
  },
  saveState : function(grid, state) {
    state.start = grid.store.lastOptions.params.start;
  },
  onStateChange : function(ev, store, records, options) {
    if (ev == "load") {this.grid.saveState(); };
  }
});

Basically we’re intercepting the attached Grid’s saveState() event and appending the current start value as stored in the Grid’s DataStore (e.g. if you’re looking at page 3 with 25 rows per page then start = 50). If you examine the contents of your state provider using Firebug (Ext.state.Manager.getProvider().state, then look for the key that matches the id of your grid), you’ll see that there is now a record for ‘start’, which grabbed the correct value from the Grid’s store.

All you need to do then is retrieve that value from the state provider and load your store accordingly:

store = new Ext.data.Store({... your store config ...});

grid = new Ext.grid.GridPanel({
  id: 'unique_grid_id',
  store: store,
  ... other grid config ...
});

//shorthand way of retrieving state information
var state = Ext.state.Manager.getProvider();

var start = state.get(options.id).start || 0);
store.load({params: {start: start, limit: 25}});

If the start value for this grid has never been set it’ll default to zero – e.g. the first page. Next time you come back to this grid it’ll take you right back to where you were, including all column setup and sorting behaviour you have specified.

%d bloggers like this: