Adding a ComboBox editor to a ExtJS EditorGridPanel column can by tricky if your ComboBox uses a remote store. This post demonstrates using a custom grid column type and associated renderer to solve this problem.

ExtJS EditorGridPanel

The ExtJS EditorGrid Panel is excellent for editing records from within the grid. The component allows one to specify an editor type per column. The ExtJS 3 examples show a nice example of this

The problem

The editor works perfectly for columns in which the stored value is also the display value. The EditorGridPanel also allows for a ComboBox to be used as the editor. When the displayField and valueField of the combobox are the same this works ok, but less so for columns that are for instance foreign keys into other models. An example illustrates this:

In this example the Assignee column lists the ID’s of users. A ComboBox has been specified as the editor. When editing the field, the ComboBox comes into action and correctly lists the Users by name:

After selecting a User the column value is updated with the user’s id. This allows us to change the column value easily, but of course this is not what we want. Wat we actually want is to display the User’s name in the grid, hiding the value. At the same time the column’s raw value should still be the user id:

A number of specialized ComboBox renderers have been published in the Sencha forum that allow to do just this. The renderer looks up the corresponding record in the ComboBox’s store by it’s valueField and returns the corresponding displayField. For ComboBoxes with local stores that usually works but without hacks this will not fly for ComboBoxes with remote stores such as a JsonStore or DirectStore. This has to do with the fact that at the time the grid is created and the custom renderer is asked for the record’s displayField the ComboBoxes store has not finished loading yet. The only times this does work is if you get luck and win the race condition… Not what we want at all in production applications.

The solution

I am releasing a custom grid column with associated renderer that solves exactly this problem. The only thing the column configuration needs is the EditorGrid’s id and an editor that we are specifying anyway. The renderer is setup automatically. The way this works is that the renderer checks if the ComboBox editor’s store has already been loaded. If this is not the case it sets up a one-time event listener on the ComboBox store’s ‘load’ event. The event handler lets the Editor Grid’s view refresh, essentially re-calling the columns renderers. This time the store is loaded and the record and displayField can be found.

The following code snipped explains the configuration neccessary:

/* create the ComboBox editor */
var userCombo = new Ext.ComboBox({
	id: 'myCombo',
	valueField:'id',
	displayField:'name',
	store: ...
});

/* We need a unique id for the grid, either create one yourself or let Ext create it for you */

var gridId = Ext.id();

/* Create the grid */

var todoGrid = new Ext.grid.EditorGridPanel({
	store: myStore,
	id: gridId, 					// Be sure to include the grid id
	cm: new Ext.grid.ColumnModel({
		columns: [{
			id: 'todo',
			header: 'Todo',
			dataIndex: 'todo'
		},{
			id: 'owner',
			width: 150,
			header: 'Assignee',
			dataIndex: 'owner',
			xtype: 'combocolumn', 	// Use the custom column or use the column's render manually
			editor: userCombo,		// The custom column needs a ComboBox editor to be able to render the displayValue, without it just renders value
			gridId: gridId			// Don't forget to specify the grid's id, the columns renderer needs it
		}]
	}),
	clicksToEdit: 1
});

And VOILA, an EditorGrid that displays the record by it’s displayField while still functioning exactly the same and also working with remote stores. The complete custom column specification and renderer is displayed below and also attached as a zip file.

The code

/**
 * @class Ext.ux.grid.ComboColumn
 * @extends Ext.grid.Column
 *
 * A Column definition class which renders a value using a ComboBox editor. The ComboBox is used to
 * convert the column value to a display value using the ComboBox's valueField and displayField.
 * If the ComboBox editor uses a remote store the column definition needs to be passed the grid's id.
 * See the {@Ext.ux.grid.ComboColumn#gridId gridId} config option of {@link Ext.ux.grid.ComboColumn}
 * for more details.
 *
 * @author    Rob Boerman
 * @copyright (c) 2011, by Rob Boerman
 * @date      20. May 2011
 * @version   1.0

Example:

var userCombo = new Ext.ComboBox({
	id: 'myCombo',
	valueField:'id',
	displayField:'name',
	store: ...
});

* We need a unique id for the grid, either create one yourself or let Ext create it for you

var gridId = Ext.id();

* Create the grid

var todoGrid = new Ext.grid.EditorGridPanel({
	store: myStore,
	id: gridId, 					// Be sure to include the grid id
	cm: new Ext.grid.ColumnModel({
		columns: [{
			id: 'todo',
			header: 'Todo',
			dataIndex: 'todo'
		},{
			id: 'owner',
			width: 150,
			header: 'Assignee',
			dataIndex: 'owner',
			xtype: 'combocolumn', 	// Use the custom column or use the column's render manually
			editor: userCombo,		// The custom column needs a ComboBox editor to be able to render the displayValue, without it just renders value
			gridId: gridId			// Don't forget to specify the grid's id, the columns renderer needs it
		}]
	}),
	clicksToEdit: 1
});

 * @license Ext.ux.grid.ComboColumn is licensed under the terms of
 * the Open Source LGPL 3.0 license.  Commercial use is permitted to the extent
 * that the code/component(s) do NOT become part of another Open Source or Commercially
 * licensed development library or toolkit without explicit permission.
 *
 *

License details: <a href="http://www.gnu.org/licenses/lgpl.html" target="_blank">http://www.gnu.org/licenses/lgpl.html</a>

 *
 * @donate
 *

<form action="https://www.paypal.com/cgi-bin/webscr" method="post">
 *
<input name="cmd" type="hidden" value="_donations" />
 *
<input name="business" type="hidden" value="ZRRCZDNRCVVEE" />
 *
<input name="lc" type="hidden" value="NL" />
 *
<input name="item_name" type="hidden" value="robboerman.com" />
 *
<input name="currency_code" type="hidden" value="EUR" />
 *
<input name="bn" type="hidden" value="PP-DonationsBF:btn_donate_LG.gif:NonHosted" />
 *
<input alt="PayPal - The safer, easier way to pay online!" name="submit" src="https://www.paypalobjects.com/WEBSCR-640-20110429-1/en_US/i/btn/btn_donate_LG.gif" type="image" />
 * <img src="https://www.paypalobjects.com/WEBSCR-640-20110429-1/en_US/i/scr/pixel.gif" border="0" alt="" width="1" height="1" />
 * </form>

 */

Ext.ns("Ext.ux.renderer","Ext.ux.grid");

Ext.ux.grid.ComboColumn = Ext.extend(Ext.grid.Column, {

/**
	* @cfg {String} gridId
	*
	* The id of the grid this column is in. This is required to be able to refresh the view once the combo store has loaded
	*/
	gridId: undefined,

    constructor: function(cfg){
        Ext.ux.grid.ComboColumn.superclass.constructor.call(this, cfg);

		// Detect if there is an editor and if it at least extends a combobox, otherwise just treat it as a normal column and render the value itself
		this.renderer = (this.editor && this.editor.triggerAction) ? Ext.ux.renderer.ComboBoxRenderer(this.editor,this.gridId) : function(value) {return value;};
    }
});

Ext.grid.Column.types['combocolumn'] = Ext.ux.grid.ComboColumn;

/* a renderer that makes a editorgrid panel render the correct value */
Ext.ux.renderer.ComboBoxRenderer = function(combo, gridId) {
	/* Get the displayfield from the store or return the value itself if the record cannot be found */
	var getValue = function(value) {
		var idx = combo.store.find(combo.valueField, value);
		var rec = combo.store.getAt(idx);
		if (rec) {
			return rec.get(combo.displayField);
		}
		return value;
	}

	return function(value) {
		/* If we are trying to load the displayField from a store that is not loaded, add a single listener to the combo store's load event to refresh the grid view */
		if (combo.store.getCount() == 0 && gridId) {
			combo.store.on(
				'load',
				function() {
					var grid = Ext.getCmp(gridId);
					if (grid) {
						grid.getView().refresh();
					}
				},
				{
					single: true
				}
			);
			return value;
		}

		return getValue(value);
	};
};

Good luck and don’t forget to share this post if it’s of use to you, it might also be to others.

Rob

Ext.ux.ComboColumn source file

Tagged with:
 

36 Responses to ComboBox editor (remote) and renderer for ExtJS EditorGridPanel

  1. Ed Spencer says:

    Nice writeup Rob. See you at SenchaCon 2011 in October?

    • admin says:

      Hi Ed,

      Thanks, If you are there, presenting a nice new featured item again (say something like Sencha Touch 2) I will definitely come :)

      Rob

  2. Stephen Bishop says:

    Thank you very much, have been looking for a solid solution for this for a while now.

  3. Durlabh Jain says:

    What happens when the store fetches only limited values based on type-ahead or pageSize? To resolve that issue, we create a “proxy” column that holds the displayValue for each row. That way, even if store has paging or filtering based on the row, data on other rows won’t get affected.

    • admin says:

      Hi,
      In that case this solution indeed does not work. I think for more complex setups a proxy column would be ideal. Thanks for the feedback

  4. Dave says:

    Thanks so much for this! Been struggling for about 3 hours trying to get a renderer setup, and this took care of it!

    Much thanks!

  5. Rafael says:

    Does this working on Ext4?

    • admin says:

      Hi Rafael,

      Honestly I don’t know whether this works on ExtJS 4. The grids in ExtJS 4 have been completely redesigned (for the better!)

      Rob

  6. gie says:

    it’s great Ed.., Thanks..

    anyway, is it possible to show all the editor components in a grid editor without clicking them first?

    so, when the editor grid is rendered, the editor component in it also appear.

    Thanks in Advanced :)

    • admin says:

      Hi Yagi,

      No, that is not possible. That would require an editor instance for every cell instead of one that is re-used for all cells from a column. The way the editor is implemented in ExtJS is that the editor is created once for a grid column, when you click a cell the editor instance is moved to that cell to edit it (and thus removed from the cell that was previously being edited. Of course you can let the grid start editing with a single click so that the overhead is minimal.

      Cheers,
      Rob

  7. Giovanni says:

    This is fantastic, i was struggling with this problem since yesterday.

    I’m very new with all this ext-js mumbo jumbo, previously i just code web in plain rails. Thanks for sharing, btw.

    • Rob Boerman says:

      Hi,

      Thanks for your comment. If that solution works better for you, go for it.
      Myself, I think it is much more involved and error prone: one has to specify a renderer manually and a custom listener for each and every ‘remote editor’ field. When I have a grid with 5 ‘foreign keys’ into other models I have to create a listener on the validateedit event and create 5 if statements in there to make it work. I would rather just use a custom column definition that does everything for me. I’m a fan of the ‘Don’t Make Me Think’ approach in complex systems :)

  8. hoanghuu says:

    It happen bug “Ext.grid.Column.types undefined” at line Ext.grid.Column.types['combocolumn'] = Ext.ux.grid.ComboColumn;

    • Rob Boerman says:

      Hi,
      make sure your column classes are loaded before you load the custom column definition. The ExtJS sources create the Ext.grid.Column.types namespace. If you load the custom sources too soon that namespace does not exist yet

      • hoanghuu says:

        I mean error “Ext.grid.Column.types is undefined”.

        I used ext js 4.x

        • Rob Boerman says:

          Hello,

          I have not tested the solution in ExtJS4. Probably it will not work because the ExtJS4 grids are completely rewritten

          Will maybe find a solution for ExtJS4 in the near future

        • Karina says:

          Hi! I had that same problem, trying to use your code with ExtJS 4.
          I could make it work replacing lines 87 – 104 with the following:

          Ext.define(‘Ext.ux.grid.ComboColumn’, {
          extend: ‘Ext.grid.Column’,
          alias: ‘widget.combocolumn’,

          /**
          * @cfg {String} gridId
          *
          * The id of the grid this column is in. This is required to be able to refresh the view once the combo store has loaded
          */
          gridId: undefined,

          constructor: function(cfg){
          Ext.ux.grid.ComboColumn.superclass.constructor.call(this, cfg);

          // Detect if there is an editor and if it at least extends a combobox, otherwise just treat it as a normal column and render the value itself
          this.renderer = (this.editor && this.editor.triggerAction) ? Ext.ux.renderer.ComboBoxRenderer(this.editor,this.gridId) : function(value) {return value;};
          }
          });

  9. Martijn says:

    Works like a charm! Perfect!

  10. lida says:

    I have a question. How to display two value fields in Ext JS combobox?

    Thanks in advance

  11. pudjo says:

    How if I want to dsplay, for example: name, date of birth when I select ID of the person combo box in grid. Name, dateof birth will dispayed in the same row of the grid..
    Regards,
    Pudjo

  12. Florent says:

    Hello and thank you for this custom and very useful column. After hours spent on the understanding of ComboBoxes and Stores, your class is my Graal :)
    I’ll spread the Word on the ModX forums.
    Thanks again.

  13. Abhishek says:

    This renderer is to refresh screen is not working if screen has more than one combo. it is loading and refresh only one combo but for others it is expecting twice DB hit as all combo is remote.

    Please suggest some solution

  14. Jim says:

    This component is exactly what I was looking for. I brought this into our project, and was testing this against a simple grid with two lookups. Each lookup was a remote data store with 12 and 19 records.

    Most of the time, this was fine (like when the grid was rendering AFTER those lookup stores have already loaded), however, when the onload event was added, I noticed a serious performance hit.

    Environment

    Ext 4.1, beta 2
    Firefox 10

    If left alone, the rendering would eventually work (like 1-2 minutes later).

    Has anyone else seen this type of thing?

    Thanks again for the great component.

  15. Luke says:

    Hey Rob! Any luck with 4.0?

    Thanks

    • Rob Boerman says:

      Hey Luke,

      Unfortunately I haven’t had time yet. Currently porting the Sencha Touch Timepicker over to ST2. After that I will try to find time for this one but the next 2 weeks are completely full

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>