This time I needed to provide a drop-down list with selectable options exactly as you see for an ExtJs combobox. However, my base field was a TextArea instead of a single-line TextField, and I needed to provide selectable items in a DropDownList as text is inputted to the TextArea.

For an analogy, compare it with your Gmail Compose To, CC and BCC fields, which are multi-line inputs and present options in a drop-down as you type into them. My situation was something similar (rather exactly identical, I am working on Mail client that is part of a larger framework for integrated access to all processes of a company’s setup from a single point).

I had faced this situation atleast one time earlier (when I needed to provide Addresses in a drop-down as they were typed into a TextArea). At that point, I employed a hack hiding the input field of the combobox behind the TextArea and manually expanding the combobox list as text changed in TextArea. However, this time, I decided to go for a more logical approach and create a re-usable extension that can be used easily down-the-line for similar situations.

Probably another reason I decided to create an extension was that I wanted to be able to use the same DropDownList with multiple fields on the page instead of having to create a new DropDown for each field. Before discussing the extension forward, you can quickly test the extension below (try typing names starting with ‘a’, or ‘c’ or ‘j’ etc):

 

You can attach the drop-down list to any form field (rather any Ext.Element) with a single line, and handle the selection of the items as it works for you. In the above example, all 3 fields share the same DropDownList rather than creating individual instance of it.

Here’s the code for the DropDownList extension:

 

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1; }//DropDownList
Ext.ns(‘Ext.ux’);
Ext.ux.DropDownList = Ext.extend(Ext.Layer, {
store: null,
valueField: null,
innerList: null,
view: null,
inKeyMode: false,
selectedIndex: -1,
currentEl: null,
keyNav: null,

constructor: function(config, existingEl) {
var cls = ‘x-combo-list’;

Ext.applyIf(config, {
parentEl: Ext.getBody(),
cls: cls,
constrain: false,
width: 200,
height: 100,
//Store options
fields: [‘text’],
valueField: ‘text’,
data: [],
//DataView options
selectedClass: ‘x-combo-selected’,
singleSelect: true,
tpl: ‘<tpl for=”.”><div class=”‘ + cls + ‘-item”>{text}</div></tpl>’,
listeners: {}
});

Ext.ux.DropDownList.superclass.constructor.call(this, config, existingEl);

this.valueField = config.valueField;

this.store = new Ext.data.JsonStore({
fields: config.fields,
data: config.data
});

this.setSize(config.width, config.height);
this.swallowEvent(‘mousewheel’);

this.innerList = this.createChild({ cls: cls + ‘-inner’ });
this.innerList.setSize(config.width – this.getFrameWidth(‘lr’), config.height – this.getFrameWidth(‘tb’));

this.innerList.on(‘mouseover’, this.onViewOver, this);
this.innerList.on(‘mousemove’, this.onViewMove, this);

this.view = new Ext.DataView({
applyTo: this.innerList,
tpl: config.tpl,
singleSelect: config.singleSelect,
itemSelector: ‘.’ + cls + ‘-item’,
selectedClass: config.selectedClass,
//overClass: config.selectedClass,
//emptyText: this.listEmptyText,
//deferEmptyText: false,
store: this.store,
listeners: config.listeners
});
this.view.addEvents(‘itemselected’, ‘processquery’, ‘listclosed’, ‘listuserclosed’);

this.view.on({
containerclick: this.onViewClick,
click: this.onViewClick,
scope: this
});
},

clearFilter: function() {
this.store.clearFilter();
},

bindElement: function(el) {
this.unbindCurrentElement();

this.currentEl = el;
el.on(‘keyup’, this.currentElKeyUp, this);

this.alignTo(this.currentEl);
this.keyNav = new Ext.KeyNav(el, {
“up”: function(e) {
if (!this.isVisible()) {
//this.alignTo(el);
//this.show();
return (true);
} else {
this.inKeyMode = true;
this.selectPrev();
}
},

“down”: function(e) {
if (!this.isVisible()) {
//this.alignTo(el);
//this.show();
return (true);
} else {
this.inKeyMode = true;
this.selectNext();
}
},

“enter”: function(e) {
this.onViewClick();
},

“esc”: function(e) {
if (this.isVisible()) {
this.collapse(true);
}
},

“tab”: function(e) {
//if (this.forceSelection === true) {
// this.collapse();
//} else {
this.onViewClick(false);
//}
return true;
},

scope: this
});
},

unbindCurrentElement: function() {
if (this.keyNav != null) {
this.keyNav.destroy();
}
if (this.currentEl != null) {
this.currentEl.un(‘keyup’, this.currentElKeyUp);
}

this.currentEl = null;
if (this.isVisible()) {
this.collapse(false);
}
},

select: function(index, scrollIntoView) {
this.selectedIndex = index;
this.view.select(index);
if (scrollIntoView !== false) {
var el = this.view.getNode(index);
if (el) {
this.innerList.scrollChildIntoView(el, false);
}
}

},

selectNext: function() {
var ct = this.store.getCount();
if (ct > 0) {
if (this.selectedIndex == -1) {
this.select(0);
} else if (this.selectedIndex < ct – 1) {
this.select(this.selectedIndex + 1);
}
}
},

selectPrev: function() {
var ct = this.store.getCount();
if (ct > 0) {
if (this.selectedIndex == -1) {
this.select(0);
} else if (this.selectedIndex !== 0) {
this.select(this.selectedIndex – 1);
}
}
},

currentElKeyUp: function(e, t) {
if (e.isNavKeyPress()) {
return;
}
var val = this.currentEl.getValue();
var options = { query: val };
this.view.fireEvent(‘processquery’, this, options);

this.store.filter(this.valueField, options.query, false, false);
if (this.store.getCount() > 0) {
if (!this.isVisible()) {
this.alignTo(this.currentEl);
this.show();
}
this.select(0, true);
} else {
if (this.isVisible()) {
this.collapse(false);
}
}
},

onViewOver: function(e, t) {
if (this.inKeyMode) { // prevent key nav and mouse over conflicts
return;
}
var item = this.view.findItemFromChild(t);
if (item) {
var index = this.view.indexOf(item);
this.select(index, false);
}
},

onViewMove: function(e, t) {
this.inKeyMode = false;
},

onViewClick: function(doFocus) {
var index = this.view.getSelectedIndexes()[0],
s = this.store,
r = s.getAt(index);
if (r) {
this.view.fireEvent(‘itemselected’, this, r, index);
this.collapse(false);
} else {
this.collapse(false);
}
if (doFocus !== false) {
this.currentEl.focus();
}
},

collapse: function(userClose) {
if (this.isVisible()) {
this.hide();
if (userClose) {
this.view.fireEvent(‘listuserclosed’);
}
this.view.fireEvent(‘listclosed’);
}
}
});{/syntaxhighlighter}

 

 

You can see from the code that I have basically embedded an Ext.DataView into an Ext.Layer and provided methods and events to interact with the extension. The store and any other objects required for the DataView are instantized internally by the extension itself. I have to agree that I studied ExtJs ComboBox code to create this extension.

Here’s how you would instantiate an instance of this extension:

 

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1; }var contactsDropDown = new Ext.ux.DropDownList({
fields: [‘name’, ’email’],
valueField: ‘name’,
data: [
{ name: ‘Andrew’, email: ‘[email protected]’ },
{ name: ‘John’, email: ‘[email protected]’ },
{ name: ‘Anand’, email: ‘[email protected]’ },
{ name: ‘Akhil’, email: ‘[email protected]’ },
{ name: ‘Charles’, email: ‘[email protected]’ }
],
tpl: ‘<tpl for=”.”><div class=”x-combo-list-item”>{name} &lt;{email}&gt;</div></tpl>’,
width: 400,
listeners: {
processquery: function(list, options) {
//Process the query if you need to here.
},

itemselected: function(list, record, index) {
//Process the selected item from the drop-down here.
}
}
});{/syntaxhighlighter}

Here are the interesting points in the above code:

  • In the config object for the extension, you pass the fields (the store fields) for the DataView together with the data also for the extension.
    The extension uses a JsonStore and therefore the data needs to be json object for each record.
  • You can customize each item in drop-down list with an XTemplate tpl.
  • ‘itemselected’ event notifies you that an item has been selected (corresponds to the select event for the ComboBox).
  • Other helper events are also available (‘processquery’ to change the query if desired before filtering the list, ‘listclosed’ when list collpases for any reason and ‘listuserclosed’ when list is closed explicitly by the user by pressing “Esc” key).
  • Various other config options like width, height, list class etc. are also supported.

Here are the 3 lines, for associating the shared list with each TextArea as it receives focus:

 

{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1; }Ext.getCmp(‘txt1’).on(‘focus’, function() { contactsDropDown.bindElement(Ext.getCmp(‘txt1’).el); });
Ext.getCmp(‘txt2’).on(‘focus’, function() { contactsDropDown.bindElement(Ext.getCmp(‘txt2’).el); });
Ext.getCmp(‘txt3’).on(‘focus’, function() { contactsDropDown.bindElement(Ext.getCmp(‘txt3’).el); });{/syntaxhighlighter}

As you can see, its pretty easy to associate the DropDownList with virtually any ExtJs Element (you bind the List to an Ext.Element, so it can be attached to any Html element directly by wrapping it in an Ext.Element).

There are many ways in which this extension can be improved. e.g. Instead of passing in data, you can change it to pass in a Store itself, so that data in the drop-down can be fetched easily from the server, or use non-JsonStores with it.

However, that should be easy to accomplish by modifying this extension. As it currently stands, it provided me what I needed in my scenario.

Attached with this post is the source for the Extension, as well as the html page for the above demonstration.