Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / form / field / ComboBox.js
index e3db662..834656c 100644 (file)
@@ -1,6 +1,19 @@
+/*
+
+This file is part of Ext JS 4
+
+Copyright (c) 2011 Sencha Inc
+
+Contact:  http://www.sencha.com/contact
+
+GNU General Public License Usage
+This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
+
+If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
+
+*/
 /**
- * @class Ext.form.field.ComboBox
- * @extends Ext.form.field.Picker
+ * @docauthor Jason Johnston <jason@sencha.com>
  *
  * A combobox control with support for autocomplete, remote loading, and many other features.
  *
  * If your store is not remote, i.e. it depends only on local data and is loaded up front, you should be
  * sure to set the {@link #queryMode} to `'local'`, as this will improve responsiveness for the user.
  *
- * {@img Ext.form.ComboBox/Ext.form.ComboBox.png Ext.form.ComboBox component}
- *
- * ## Example usage:
+ * # Example usage:
  *
+ *     @example
  *     // The data store containing the list of states
  *     var states = Ext.create('Ext.data.Store', {
  *         fields: ['abbr', 'name'],
@@ -41,7 +53,7 @@
  *         renderTo: Ext.getBody()
  *     });
  *
- * ## Events
+ * # Events
  *
  * To do something when something in ComboBox is selected, configure the select event:
  *
  *     var cb = new Ext.form.field.ComboBox(yourOptions);
  *     cb.on('select', yourFunction, yourScope);
  *
- * ## Multiple Selection
+ * # Multiple Selection
  *
  * ComboBox also allows selection of multiple items from the list; to enable multi-selection set the
  * {@link #multiSelect} config to `true`.
- *
- * @constructor
- * Create a new ComboBox.
- * @param {Object} config Configuration options
- * @xtype combo
- * @docauthor Jason Johnston <jason@sencha.com>
  */
 Ext.define('Ext.form.field.ComboBox', {
     extend:'Ext.form.field.Picker',
@@ -75,209 +81,267 @@ Ext.define('Ext.form.field.ComboBox', {
     alias: ['widget.combobox', 'widget.combo'],
 
     /**
-     * @cfg {String} triggerCls
-     * An additional CSS class used to style the trigger button. The trigger will always get the
-     * {@link #triggerBaseCls} by default and <tt>triggerCls</tt> will be <b>appended</b> if specified.
-     * Defaults to 'x-form-arrow-trigger' for ComboBox.
+     * @cfg {String} [triggerCls='x-form-arrow-trigger']
+     * An additional CSS class used to style the trigger button. The trigger will always get the {@link #triggerBaseCls}
+     * by default and `triggerCls` will be **appended** if specified.
      */
     triggerCls: Ext.baseCSSPrefix + 'form-arrow-trigger',
 
     /**
-     * @cfg {Ext.data.Store/Array} store The data source to which this combo is bound (defaults to <tt>undefined</tt>).
-     * Acceptable values for this property are:
-     * <div class="mdetail-params"><ul>
-     * <li><b>any {@link Ext.data.Store Store} subclass</b></li>
-     * <li><b>an Array</b> : Arrays will be converted to a {@link Ext.data.Store} internally,
-     * automatically generating {@link Ext.data.Field#name field names} to work with all data components.
-     * <div class="mdetail-params"><ul>
-     * <li><b>1-dimensional array</b> : (e.g., <tt>['Foo','Bar']</tt>)<div class="sub-desc">
-     * A 1-dimensional array will automatically be expanded (each array item will be used for both the combo
-     * {@link #valueField} and {@link #displayField})</div></li>
-     * <li><b>2-dimensional array</b> : (e.g., <tt>[['f','Foo'],['b','Bar']]</tt>)<div class="sub-desc">
-     * For a multi-dimensional array, the value in index 0 of each item will be assumed to be the combo
-     * {@link #valueField}, while the value at index 1 is assumed to be the combo {@link #displayField}.
-     * </div></li></ul></div></li></ul></div>
-     * <p>See also <tt>{@link #queryMode}</tt>.</p>
+     * @private
+     * @cfg {String}
+     * CSS class used to find the {@link #hiddenDataEl}
+     */
+    hiddenDataCls: Ext.baseCSSPrefix + 'hide-display ' + Ext.baseCSSPrefix + 'form-data-hidden',
+
+    /**
+     * @override
+     */
+    fieldSubTpl: [
+        '<div class="{hiddenDataCls}" role="presentation"></div>',
+        '<input id="{id}" type="{type}" ',
+            '<tpl if="size">size="{size}" </tpl>',
+            '<tpl if="tabIdx">tabIndex="{tabIdx}" </tpl>',
+            'class="{fieldCls} {typeCls}" autocomplete="off" />',
+        '<div id="{cmpId}-triggerWrap" class="{triggerWrapCls}" role="presentation">',
+            '{triggerEl}',
+            '<div class="{clearCls}" role="presentation"></div>',
+        '</div>',
+        {
+            compiled: true,
+            disableFormats: true
+        }
+    ],
+
+    getSubTplData: function(){
+        var me = this;
+        Ext.applyIf(me.subTplData, {
+            hiddenDataCls: me.hiddenDataCls
+        });
+        return me.callParent(arguments);
+    },
+
+    afterRender: function(){
+        var me = this;
+        me.callParent(arguments);
+        me.setHiddenValue(me.value);
+    },
+
+    /**
+     * @cfg {Ext.data.Store/Array} store
+     * The data source to which this combo is bound. Acceptable values for this property are:
+     *
+     *   - **any {@link Ext.data.Store Store} subclass**
+     *   - **an Array** : Arrays will be converted to a {@link Ext.data.Store} internally, automatically generating
+     *     {@link Ext.data.Field#name field names} to work with all data components.
+     *
+     *     - **1-dimensional array** : (e.g., `['Foo','Bar']`)
+     *
+     *       A 1-dimensional array will automatically be expanded (each array item will be used for both the combo
+     *       {@link #valueField} and {@link #displayField})
+     *
+     *     - **2-dimensional array** : (e.g., `[['f','Foo'],['b','Bar']]`)
+     *
+     *       For a multi-dimensional array, the value in index 0 of each item will be assumed to be the combo
+     *       {@link #valueField}, while the value at index 1 is assumed to be the combo {@link #displayField}.
+     *
+     * See also {@link #queryMode}.
      */
 
     /**
      * @cfg {Boolean} multiSelect
-     * If set to <tt>true</tt>, allows the combo field to hold more than one value at a time, and allows selecting
-     * multiple items from the dropdown list. The combo's text field will show all selected values separated by
-     * the {@link #delimiter}. (Defaults to <tt>false</tt>.)
+     * If set to `true`, allows the combo field to hold more than one value at a time, and allows selecting multiple
+     * items from the dropdown list. The combo's text field will show all selected values separated by the
+     * {@link #delimiter}.
      */
     multiSelect: false,
 
     /**
      * @cfg {String} delimiter
-     * The character(s) used to separate the {@link #displayField display values} of multiple selected items
-     * when <tt>{@link #multiSelect} = true</tt>. Defaults to <tt>', '</tt>.
+     * The character(s) used to separate the {@link #displayField display values} of multiple selected items when
+     * `{@link #multiSelect} = true`.
      */
     delimiter: ', ',
 
     /**
-     * @cfg {String} displayField The underlying {@link Ext.data.Field#name data field name} to bind to this
-     * ComboBox (defaults to 'text').
-     * <p>See also <tt>{@link #valueField}</tt>.</p>
+     * @cfg {String} displayField
+     * The underlying {@link Ext.data.Field#name data field name} to bind to this ComboBox.
+     *
+     * See also `{@link #valueField}`.
      */
     displayField: 'text',
 
     /**
-     * @cfg {String} valueField
-     * @required
+     * @cfg {String} valueField (required)
      * The underlying {@link Ext.data.Field#name data value name} to bind to this ComboBox (defaults to match
      * the value of the {@link #displayField} config).
-     * <p><b>Note</b>: use of a <tt>valueField</tt> requires the user to make a selection in order for a value to be
-     * mapped. See also <tt>{@link #displayField}</tt>.</p>
+     *
+     * **Note**: use of a `valueField` requires the user to make a selection in order for a value to be mapped. See also
+     * `{@link #displayField}`.
      */
 
     /**
-     * @cfg {String} triggerAction The action to execute when the trigger is clicked.
-     * <div class="mdetail-params"><ul>
-     * <li><b><tt>'all'</tt></b> : <b>Default</b>
-     * <p class="sub-desc">{@link #doQuery run the query} specified by the <tt>{@link #allQuery}</tt> config option</p></li>
-     * <li><b><tt>'query'</tt></b> :
-     * <p class="sub-desc">{@link #doQuery run the query} using the {@link Ext.form.field.Base#getRawValue raw value}.</p></li>
-     * </ul></div>
-     * <p>See also <code>{@link #queryParam}</code>.</p>
+     * @cfg {String} triggerAction
+     * The action to execute when the trigger is clicked.
+     *
+     *   - **`'all'`** :
+     *
+     *     {@link #doQuery run the query} specified by the `{@link #allQuery}` config option
+     *
+     *   - **`'query'`** :
+     *
+     *     {@link #doQuery run the query} using the {@link Ext.form.field.Base#getRawValue raw value}.
+     *
+     * See also `{@link #queryParam}`.
      */
     triggerAction: 'all',
 
     /**
-     * @cfg {String} allQuery The text query to send to the server to return all records for the list
-     * with no filtering (defaults to '')
+     * @cfg {String} allQuery
+     * The text query to send to the server to return all records for the list with no filtering
      */
     allQuery: '',
 
     /**
-     * @cfg {String} queryParam Name of the query ({@link Ext.data.proxy.Proxy#extraParam extraParam} name for the store)
-     * as it will be passed on the querystring (defaults to <tt>'query'</tt>). If explicitly set to a falsey value it will
-     * not be send.
+     * @cfg {String} queryParam
+     * Name of the parameter used by the Store to pass the typed string when the ComboBox is configured with
+     * `{@link #queryMode}: 'remote'`. If explicitly set to a falsy value it will not be sent.
      */
     queryParam: 'query',
 
     /**
      * @cfg {String} queryMode
-     * The mode for queries. Acceptable values are:
-     * <div class="mdetail-params"><ul>
-     * <li><b><tt>'remote'</tt></b> : <b>Default</b>
-     * <p class="sub-desc">Automatically loads the <tt>{@link #store}</tt> the <b>first</b> time the trigger
-     * is clicked. If you do not want the store to be automatically loaded the first time the trigger is
-     * clicked, set to <tt>'local'</tt> and manually load the store.  To force a requery of the store
-     * <b>every</b> time the trigger is clicked see <tt>{@link #lastQuery}</tt>.</p></li>
-     * <li><b><tt>'local'</tt></b> :
-     * <p class="sub-desc">ComboBox loads local data</p>
-     * <pre><code>
-var combo = new Ext.form.field.ComboBox({
-    renderTo: document.body,
-    queryMode: 'local',
-    store: new Ext.data.ArrayStore({
-        id: 0,
-        fields: [
-            'myId',  // numeric value is the key
-            'displayText'
-        ],
-        data: [[1, 'item1'], [2, 'item2']]  // data is local
-    }),
-    valueField: 'myId',
-    displayField: 'displayText',
-    triggerAction: 'all'
-});
-     * </code></pre></li>
-     * </ul></div>
+     * The mode in which the ComboBox uses the configured Store. Acceptable values are:
+     *
+     *   - **`'remote'`** :
+     *
+     *     In `queryMode: 'remote'`, the ComboBox loads its Store dynamically based upon user interaction.
+     *
+     *     This is typically used for "autocomplete" type inputs, and after the user finishes typing, the Store is {@link
+     *     Ext.data.Store#load load}ed.
+     *
+     *     A parameter containing the typed string is sent in the load request. The default parameter name for the input
+     *     string is `query`, but this can be configured using the {@link #queryParam} config.
+     *
+     *     In `queryMode: 'remote'`, the Store may be configured with `{@link Ext.data.Store#remoteFilter remoteFilter}:
+     *     true`, and further filters may be _programatically_ added to the Store which are then passed with every load
+     *     request which allows the server to further refine the returned dataset.
+     *
+     *     Typically, in an autocomplete situation, {@link #hideTrigger} is configured `true` because it has no meaning for
+     *     autocomplete.
+     *
+     *   - **`'local'`** :
+     *
+     *     ComboBox loads local data
+     *
+     *         var combo = new Ext.form.field.ComboBox({
+     *             renderTo: document.body,
+     *             queryMode: 'local',
+     *             store: new Ext.data.ArrayStore({
+     *                 id: 0,
+     *                 fields: [
+     *                     'myId',  // numeric value is the key
+     *                     'displayText'
+     *                 ],
+     *                 data: [[1, 'item1'], [2, 'item2']]  // data is local
+     *             }),
+     *             valueField: 'myId',
+     *             displayField: 'displayText',
+     *             triggerAction: 'all'
+     *         });
      */
     queryMode: 'remote',
 
     queryCaching: true,
 
     /**
-     * @cfg {Number} pageSize If greater than <tt>0</tt>, a {@link Ext.toolbar.Paging} is displayed in the
-     * footer of the dropdown list and the {@link #doQuery filter queries} will execute with page start and
-     * {@link Ext.toolbar.Paging#pageSize limit} parameters. Only applies when <tt>{@link #queryMode} = 'remote'</tt>
-     * (defaults to <tt>0</tt>).
+     * @cfg {Number} pageSize
+     * If greater than `0`, a {@link Ext.toolbar.Paging} is displayed in the footer of the dropdown list and the
+     * {@link #doQuery filter queries} will execute with page start and {@link Ext.view.BoundList#pageSize limit}
+     * parameters. Only applies when `{@link #queryMode} = 'remote'`.
      */
     pageSize: 0,
 
     /**
-     * @cfg {Number} queryDelay The length of time in milliseconds to delay between the start of typing and
-     * sending the query to filter the dropdown list (defaults to <tt>500</tt> if <tt>{@link #queryMode} = 'remote'</tt>
-     * or <tt>10</tt> if <tt>{@link #queryMode} = 'local'</tt>)
+     * @cfg {Number} queryDelay
+     * The length of time in milliseconds to delay between the start of typing and sending the query to filter the
+     * dropdown list (defaults to `500` if `{@link #queryMode} = 'remote'` or `10` if `{@link #queryMode} = 'local'`)
      */
 
     /**
-     * @cfg {Number} minChars The minimum number of characters the user must type before autocomplete and
-     * {@link #typeAhead} activate (defaults to <tt>4</tt> if <tt>{@link #queryMode} = 'remote'</tt> or <tt>0</tt> if
-     * <tt>{@link #queryMode} = 'local'</tt>, does not apply if <tt>{@link Ext.form.field.Trigger#editable editable} = false</tt>).
+     * @cfg {Number} minChars
+     * The minimum number of characters the user must type before autocomplete and {@link #typeAhead} activate (defaults
+     * to `4` if `{@link #queryMode} = 'remote'` or `0` if `{@link #queryMode} = 'local'`, does not apply if
+     * `{@link Ext.form.field.Trigger#editable editable} = false`).
      */
 
     /**
-     * @cfg {Boolean} autoSelect <tt>true</tt> to automatically highlight the first result gathered by the data store
-     * in the dropdown list when it is opened. (Defaults to <tt>true</tt>). A false value would cause nothing in the
-     * list to be highlighted automatically, so the user would have to manually highlight an item before pressing
-     * the enter or {@link #selectOnTab tab} key to select it (unless the value of ({@link #typeAhead}) were true),
-     * or use the mouse to select a value.
+     * @cfg {Boolean} autoSelect
+     * `true` to automatically highlight the first result gathered by the data store in the dropdown list when it is
+     * opened. A false value would cause nothing in the list to be highlighted automatically, so
+     * the user would have to manually highlight an item before pressing the enter or {@link #selectOnTab tab} key to
+     * select it (unless the value of ({@link #typeAhead}) were true), or use the mouse to select a value.
      */
     autoSelect: true,
 
     /**
-     * @cfg {Boolean} typeAhead <tt>true</tt> to populate and autoselect the remainder of the text being
-     * typed after a configurable delay ({@link #typeAheadDelay}) if it matches a known value (defaults
-     * to <tt>false</tt>)
+     * @cfg {Boolean} typeAhead
+     * `true` to populate and autoselect the remainder of the text being typed after a configurable delay
+     * ({@link #typeAheadDelay}) if it matches a known value.
      */
     typeAhead: false,
 
     /**
-     * @cfg {Number} typeAheadDelay The length of time in milliseconds to wait until the typeahead text is displayed
-     * if <tt>{@link #typeAhead} = true</tt> (defaults to <tt>250</tt>)
+     * @cfg {Number} typeAheadDelay
+     * The length of time in milliseconds to wait until the typeahead text is displayed if `{@link #typeAhead} = true`
      */
     typeAheadDelay: 250,
 
     /**
      * @cfg {Boolean} selectOnTab
-     * Whether the Tab key should select the currently highlighted item. Defaults to <tt>true</tt>.
+     * Whether the Tab key should select the currently highlighted item.
      */
     selectOnTab: true,
 
     /**
-     * @cfg {Boolean} forceSelection <tt>true</tt> to restrict the selected value to one of the values in the list,
-     * <tt>false</tt> to allow the user to set arbitrary text into the field (defaults to <tt>false</tt>)
+     * @cfg {Boolean} forceSelection
+     * `true` to restrict the selected value to one of the values in the list, `false` to allow the user to set
+     * arbitrary text into the field.
      */
     forceSelection: false,
 
     /**
-     * @cfg {String} valueNotFoundText When using a name/value combo, if the value passed to setValue is not found in
-     * the store, valueNotFoundText will be displayed as the field text if defined (defaults to undefined). If this
-     * default text is used, it means there is no value set and no validation will occur on this field.
+     * @cfg {String} valueNotFoundText
+     * When using a name/value combo, if the value passed to setValue is not found in the store, valueNotFoundText will
+     * be displayed as the field text if defined. If this default text is used, it means there
+     * is no value set and no validation will occur on this field.
      */
 
     /**
-     * The value of the match string used to filter the store. Delete this property to force a requery.
-     * Example use:
-     * <pre><code>
-var combo = new Ext.form.field.ComboBox({
-    ...
-    queryMode: 'remote',
-    listeners: {
-        // delete the previous query in the beforequery event or set
-        // combo.lastQuery = null (this will reload the store the next time it expands)
-        beforequery: function(qe){
-            delete qe.combo.lastQuery;
-        }
-    }
-});
-     * </code></pre>
-     * To make sure the filter in the store is not cleared the first time the ComboBox trigger is used
-     * configure the combo with <tt>lastQuery=''</tt>. Example use:
-     * <pre><code>
-var combo = new Ext.form.field.ComboBox({
-    ...
-    queryMode: 'local',
-    triggerAction: 'all',
-    lastQuery: ''
-});
-     * </code></pre>
-     * @property lastQuery
-     * @type String
+     * @property {String} lastQuery
+     * The value of the match string used to filter the store. Delete this property to force a requery. Example use:
+     *
+     *     var combo = new Ext.form.field.ComboBox({
+     *         ...
+     *         queryMode: 'remote',
+     *         listeners: {
+     *             // delete the previous query in the beforequery event or set
+     *             // combo.lastQuery = null (this will reload the store the next time it expands)
+     *             beforequery: function(qe){
+     *                 delete qe.combo.lastQuery;
+     *             }
+     *         }
+     *     });
+     *
+     * To make sure the filter in the store is not cleared the first time the ComboBox trigger is used configure the
+     * combo with `lastQuery=''`. Example use:
+     *
+     *     var combo = new Ext.form.field.ComboBox({
+     *         ...
+     *         queryMode: 'local',
+     *         triggerAction: 'all',
+     *         lastQuery: ''
+     *     });
      */
 
     /**
@@ -294,31 +358,28 @@ var combo = new Ext.form.field.ComboBox({
     },
 
     /**
-     * @cfg {Mixed} transform
-     * The id, DOM node or {@link Ext.core.Element} of an existing HTML <tt>&lt;select&gt;</tt> element to
-     * convert into a ComboBox. The target select's options will be used to build the options in the ComboBox
-     * dropdown; a configured {@link #store} will take precedence over this.
+     * @cfg {String/HTMLElement/Ext.Element} transform
+     * The id, DOM node or {@link Ext.Element} of an existing HTML `<select>` element to convert into a ComboBox. The
+     * target select's options will be used to build the options in the ComboBox dropdown; a configured {@link #store}
+     * will take precedence over this.
      */
 
     /**
      * @cfg {Object} listConfig
-     * <p>An optional set of configuration properties that will be passed to the {@link Ext.view.BoundList}'s
-     * constructor. Any configuration that is valid for BoundList can be included. Some of the more useful
-     * ones are:</p>
-     * <ul>
-     *     <li>{@link Ext.view.BoundList#cls} - defaults to empty</li>
-     *     <li>{@link Ext.view.BoundList#emptyText} - defaults to empty string</li>
-     *     <li>{@link Ext.view.BoundList#getInnerTpl} - defaults to the template defined in BoundList</li>
-     *     <li>{@link Ext.view.BoundList#itemSelector} - defaults to the value defined in BoundList</li>
-     *     <li>{@link Ext.view.BoundList#loadingText} - defaults to <tt>'Loading...'</tt></li>
-     *     <li>{@link Ext.view.BoundList#minWidth} - defaults to <tt>70</tt></li>
-     *     <li>{@link Ext.view.BoundList#maxWidth} - defaults to <tt>undefined</tt></li>
-     *     <li>{@link Ext.view.BoundList#maxHeight} - defaults to <tt>300</tt></li>
-     *     <li>{@link Ext.view.BoundList#resizable} - defaults to <tt>false</tt></li>
-     *     <li>{@link Ext.view.BoundList#shadow} - defaults to <tt>'sides'</tt></li>
-     *     <li>{@link Ext.view.BoundList#width} - defaults to <tt>undefined</tt> (automatically set to the width
-     *         of the ComboBox field if {@link #matchFieldWidth} is true)</li>
-     * </ul>
+     * An optional set of configuration properties that will be passed to the {@link Ext.view.BoundList}'s constructor.
+     * Any configuration that is valid for BoundList can be included. Some of the more useful ones are:
+     *
+     *   - {@link Ext.view.BoundList#cls} - defaults to empty
+     *   - {@link Ext.view.BoundList#emptyText} - defaults to empty string
+     *   - {@link Ext.view.BoundList#itemSelector} - defaults to the value defined in BoundList
+     *   - {@link Ext.view.BoundList#loadingText} - defaults to `'Loading...'`
+     *   - {@link Ext.view.BoundList#minWidth} - defaults to `70`
+     *   - {@link Ext.view.BoundList#maxWidth} - defaults to `undefined`
+     *   - {@link Ext.view.BoundList#maxHeight} - defaults to `300`
+     *   - {@link Ext.view.BoundList#resizable} - defaults to `false`
+     *   - {@link Ext.view.BoundList#shadow} - defaults to `'sides'`
+     *   - {@link Ext.view.BoundList#width} - defaults to `undefined` (automatically set to the width of the ComboBox
+     *     field if {@link #matchFieldWidth} is true)
      */
 
     //private
@@ -331,10 +392,11 @@ var combo = new Ext.form.field.ComboBox({
             transform = me.transform,
             transformSelect, isLocalMode;
 
+        Ext.applyIf(me.renderSelectors, {
+            hiddenDataEl: '.' + me.hiddenDataCls.split(' ').join('.')
+        });
+        
         //<debug>
-        if (!store && !transform) {
-            Ext.Error.raise('Either a valid store, or a HTML select to transform, must be configured on the combo.');
-        }
         if (me.typeAhead && me.multiSelect) {
             Ext.Error.raise('typeAhead and multiSelect are mutually exclusive options -- please remove one of them.');
         }
@@ -347,32 +409,60 @@ var combo = new Ext.form.field.ComboBox({
         //</debug>
 
         this.addEvents(
-            // TODO need beforeselect?
-
             /**
              * @event beforequery
-             * Fires before all queries are processed. Return false to cancel the query or set the queryEvent's
-             * cancel property to true.
-             * @param {Object} queryEvent An object that has these properties:<ul>
-             * <li><code>combo</code> : Ext.form.field.ComboBox <div class="sub-desc">This combo box</div></li>
-             * <li><code>query</code> : String <div class="sub-desc">The query string</div></li>
-             * <li><code>forceAll</code> : Boolean <div class="sub-desc">True to force "all" query</div></li>
-             * <li><code>cancel</code> : Boolean <div class="sub-desc">Set to true to cancel the query</div></li>
-             * </ul>
+             * Fires before all queries are processed. Return false to cancel the query or set the queryEvent's cancel
+             * property to true.
+             *
+             * @param {Object} queryEvent An object that has these properties:
+             *
+             *   - `combo` : Ext.form.field.ComboBox
+             *
+             *     This combo box
+             *
+             *   - `query` : String
+             *
+             *     The query string
+             *
+             *   - `forceAll` : Boolean
+             *
+             *     True to force "all" query
+             *
+             *   - `cancel` : Boolean
+             *
+             *     Set to true to cancel the query
              */
             'beforequery',
 
-            /*
+            /**
              * @event select
              * Fires when at least one list item is selected.
              * @param {Ext.form.field.ComboBox} combo This combo box
              * @param {Array} records The selected records
              */
-            'select'
+            'select',
+
+            /**
+             * @event beforeselect
+             * Fires before the selected item is added to the collection
+             * @param {Ext.form.field.ComboBox} combo This combo box
+             * @param {Ext.data.Record} record The selected record
+             * @param {Number} index The index of the selected record
+             */
+            'beforeselect',
+
+            /**
+             * @event beforedeselect
+             * Fires before the deselected item is removed from the collection
+             * @param {Ext.form.field.ComboBox} combo This combo box
+             * @param {Ext.data.Record} record The deselected record
+             * @param {Number} index The index of the deselected record
+             */
+            'beforedeselect'
         );
 
         // Build store from 'transform' HTML select element's options
-        if (!store && transform) {
+        if (transform) {
             transformSelect = Ext.getDom(transform);
             if (transformSelect) {
                 store = Ext.Array.map(Ext.Array.from(transformSelect.options), function(option) {
@@ -387,7 +477,7 @@ var combo = new Ext.form.field.ComboBox({
             }
         }
 
-        me.bindStore(store, true);
+        me.bindStore(store || 'ext-empty-store', true);
         store = me.store;
         if (store.autoCreated) {
             me.queryMode = 'local';
@@ -413,7 +503,7 @@ var combo = new Ext.form.field.ComboBox({
         if (!me.displayTpl) {
             me.displayTpl = Ext.create('Ext.XTemplate',
                 '<tpl for=".">' +
-                    '{[typeof values === "string" ? values : values.' + me.displayField + ']}' +
+                    '{[typeof values === "string" ? values : values["' + me.displayField + '"]]}' +
                     '<tpl if="xindex < xcount">' + me.delimiter + '</tpl>' +
                 '</tpl>'
             );
@@ -438,14 +528,17 @@ var combo = new Ext.form.field.ComboBox({
         }
     },
 
+    /**
+     * Returns the store associated with this ComboBox.
+     * @return {Ext.data.Store} The store
+     */
+    getStore : function(){
+        return this.store;
+    },
+
     beforeBlur: function() {
-        var me = this;
-        me.doQueryTask.cancel();
-        if (me.forceSelection) {
-            me.assertValue();
-        } else {
-            me.collapse();
-        }
+        this.doQueryTask.cancel();
+        this.assertValue();
     },
 
     // private
@@ -454,20 +547,22 @@ var combo = new Ext.form.field.ComboBox({
             value = me.getRawValue(),
             rec;
 
-        if (me.multiSelect) {
-            // For multiselect, check that the current displayed value matches the current
-            // selection, if it does not then revert to the most recent selection.
-            if (value !== me.getDisplayValue()) {
-                me.setValue(me.lastSelection);
-            }
-        } else {
-            // For single-select, match the displayed value to a record and select it,
-            // if it does not match a record then revert to the most recent selection.
-            rec = me.findRecordByDisplay(value);
-            if (rec) {
-                me.select(rec);
+        if (me.forceSelection) {
+            if (me.multiSelect) {
+                // For multiselect, check that the current displayed value matches the current
+                // selection, if it does not then revert to the most recent selection.
+                if (value !== me.getDisplayValue()) {
+                    me.setValue(me.lastSelection);
+                }
             } else {
-                me.setValue(me.lastSelection);
+                // For single-select, match the displayed value to a record and select it,
+                // if it does not match a record then revert to the most recent selection.
+                rec = me.findRecordByDisplay(value);
+                if (rec) {
+                    me.select(rec);
+                } else {
+                    me.setValue(me.lastSelection);
+                }
             }
         }
         me.collapse();
@@ -508,7 +603,7 @@ var combo = new Ext.form.field.ComboBox({
         // would create it when we may be preping to destroy it
         if (oldStore && !initial) {
             if (oldStore !== store && oldStore.autoDestroy) {
-                oldStore.destroy();
+                oldStore.destroyStore();
             } else {
                 oldStore.un({
                     scope: me,
@@ -545,9 +640,28 @@ var combo = new Ext.form.field.ComboBox({
         var me = this,
             value = me.value;
 
-        me.syncSelection();
-        if (me.picker && !me.picker.getSelectionModel().hasSelection()) {
-            me.doAutoSelect();
+        // If performing a remote query upon the raw value...
+        if (me.rawQuery) {
+            me.rawQuery = false;
+            me.syncSelection();
+            if (me.picker && !me.picker.getSelectionModel().hasSelection()) {
+                me.doAutoSelect();
+            }
+        }
+        // If store initial load or triggerAction: 'all' trigger click.
+        else {
+            // Set the value on load
+            if (me.value) {
+                me.setValue(me.value);
+            } else {
+                // There's no value.
+                // Highlight the first item in the list if autoSelect: true
+                if (me.store.getCount()) {
+                    me.doAutoSelect();
+                } else {
+                    me.setValue('');
+                }
+            }
         }
     },
 
@@ -556,19 +670,23 @@ var combo = new Ext.form.field.ComboBox({
      * Execute the query with the raw contents within the textfield.
      */
     doRawQuery: function() {
-        this.doQuery(this.getRawValue());
+        this.doQuery(this.getRawValue(), false, true);
     },
 
     /**
-     * Executes a query to filter the dropdown list. Fires the {@link #beforequery} event prior to performing the
-     * query allowing the query action to be canceled if needed.
+     * Executes a query to filter the dropdown list. Fires the {@link #beforequery} event prior to performing the query
+     * allowing the query action to be canceled if needed.
+     *
      * @param {String} queryString The SQL query to execute
-     * @param {Boolean} forceAll <tt>true</tt> to force the query to execute even if there are currently fewer
-     * characters in the field than the minimum specified by the <tt>{@link #minChars}</tt> config option.  It
-     * also clears any filter previously saved in the current store (defaults to <tt>false</tt>)
-     * @return {Boolean} true if the query was permitted to run, false if it was cancelled by a {@link #beforequery} handler.
+     * @param {Boolean} [forceAll=false] `true` to force the query to execute even if there are currently fewer characters in
+     * the field than the minimum specified by the `{@link #minChars}` config option. It also clears any filter
+     * previously saved in the current store.
+     * @param {Boolean} [rawQuery=false] Pass as true if the raw typed value is being used as the query string. This causes the
+     * resulting store load to leave the raw value undisturbed.
+     * @return {Boolean} true if the query was permitted to run, false if it was cancelled by a {@link #beforequery}
+     * handler.
      */
-    doQuery: function(queryString, forceAll) {
+    doQuery: function(queryString, forceAll, rawQuery) {
         queryString = queryString || '';
 
         // store in object and pass by reference in 'beforequery'
@@ -599,15 +717,30 @@ var combo = new Ext.form.field.ComboBox({
             // make sure they aren't querying the same thing
             if (!me.queryCaching || me.lastQuery !== queryString) {
                 me.lastQuery = queryString;
-                store.clearFilter(!forceAll);
+
                 if (isLocalMode) {
-                    if (!forceAll) {
+                    // forceAll means no filtering - show whole dataset.
+                    if (forceAll) {
+                        store.clearFilter();
+                    } else {
+                        // Clear filter, but supress event so that the BoundList is not immediately updated.
+                        store.clearFilter(true);
                         store.filter(me.displayField, queryString);
                     }
                 } else {
-                    store.load({
-                        params: me.getParams(queryString)
-                    });
+                    // Set flag for onLoad handling to know how the Store was loaded
+                    me.rawQuery = rawQuery;
+
+                    // In queryMode: 'remote', we assume Store filters are added by the developer as remote filters,
+                    // and these are automatically passed as params with every load call, so we do *not* call clearFilter.
+                    if (me.pageSize) {
+                        // if we're paging, we've changed the query so start at page 1.
+                        me.loadPage(1);
+                    } else {
+                        store.load({
+                            params: me.getParams(queryString)
+                        });
+                    }
                 }
             }
 
@@ -628,21 +761,31 @@ var combo = new Ext.form.field.ComboBox({
         return true;
     },
 
+    loadPage: function(pageNum){
+        this.store.loadPage(pageNum, {
+            params: this.getParams(this.lastQuery)
+        });
+    },
+
+    onPageChange: function(toolbar, newPage){
+        /*
+         * Return false here so we can call load ourselves and inject the query param.
+         * We don't want to do this for every store load since the developer may load
+         * the store through some other means so we won't add the query param.
+         */
+        this.loadPage(newPage);
+        return false;
+    },
+
     // private
     getParams: function(queryString) {
-        var p = {},
-            pageSize = this.pageSize,
+        var params = {},
             param = this.queryParam;
-            
+
         if (param) {
-            p[param] = queryString;
-        }
-        
-        if (pageSize) {
-            p.start = 0;
-            p.limit = pageSize;
+            params[param] = queryString;
         }
-        return p;
+        return params;
     },
 
     /**
@@ -683,7 +826,7 @@ var combo = new Ext.form.field.ComboBox({
                 if (me.triggerAction === 'all') {
                     me.doQuery(me.allQuery, true);
                 } else {
-                    me.doQuery(me.getRawValue());
+                    me.doQuery(me.getRawValue(), false, true);
                 }
             }
             me.inputEl.focus();
@@ -706,7 +849,7 @@ var combo = new Ext.form.field.ComboBox({
                 me.doQueryTask.delay(me.queryDelay);
             }
         }
-        
+
         if (me.enableKeyEvents) {
             me.callParent(arguments);
         }
@@ -717,19 +860,25 @@ var combo = new Ext.form.field.ComboBox({
         me.callParent();
 
         /*
-         * Setup keyboard handling. If enableKeyEvents is true, we already have 
+         * Setup keyboard handling. If enableKeyEvents is true, we already have
          * a listener on the inputEl for keyup, so don't create a second.
          */
         if (!me.enableKeyEvents) {
             me.mon(me.inputEl, 'keyup', me.onKeyUp, me);
         }
     },
+    
+    onDestroy: function(){
+        this.bindStore(null);
+        this.callParent();    
+    },
 
     createPicker: function() {
         var me = this,
             picker,
             menuCls = Ext.baseCSSPrefix + 'menu',
             opts = Ext.apply({
+                pickerField: me,
                 selModel: {
                     mode: me.multiSelect ? 'SIMPLE' : 'SINGLE'
                 },
@@ -740,10 +889,14 @@ var combo = new Ext.form.field.ComboBox({
                 store: me.store,
                 displayField: me.displayField,
                 focusOnToFront: false,
-                pageSize: me.pageSize
+                pageSize: me.pageSize,
+                tpl: me.tpl
             }, me.listConfig, me.defaultListConfig);
 
         picker = me.picker = Ext.create('Ext.view.BoundList', opts);
+        if (me.pageSize) {
+            picker.pagingToolbar.on('beforechange', me.onPageChange, me);
+        }
 
         me.mon(picker, {
             itemclick: me.onItemClick,
@@ -752,18 +905,34 @@ var combo = new Ext.form.field.ComboBox({
         });
 
         me.mon(picker.getSelectionModel(), {
-            selectionChange: me.onListSelectionChange,
+            'beforeselect': me.onBeforeSelect,
+            'beforedeselect': me.onBeforeDeselect,
+            'selectionchange': me.onListSelectionChange,
             scope: me
         });
 
         return picker;
     },
 
+    alignPicker: function(){
+        var me = this,
+            picker = me.picker,
+            heightAbove = me.getPosition()[1] - Ext.getBody().getScroll().top,
+            heightBelow = Ext.Element.getViewHeight() - heightAbove - me.getHeight(),
+            space = Math.max(heightAbove, heightBelow);
+
+        me.callParent();
+        if (picker.getHeight() > space) {
+            picker.setHeight(space - 5); // have some leeway so we aren't flush against
+            me.doAlign();
+        }
+    },
+
     onListRefresh: function() {
         this.alignPicker();
         this.syncSelection();
     },
-    
+
     onItemClick: function(picker, record){
         /*
          * If we're doing single selection, the selection change events won't fire when
@@ -773,25 +942,45 @@ var combo = new Ext.form.field.ComboBox({
             lastSelection = me.lastSelection,
             valueField = me.valueField,
             selected;
-        
+
         if (!me.multiSelect && lastSelection) {
             selected = lastSelection[0];
-            if (record.get(valueField) === selected.get(valueField)) {
+            if (selected && (record.get(valueField) === selected.get(valueField))) {
+                // Make sure we also update the display value if it's only partial
+                me.displayTplData = [record.data];
+                me.setRawValue(me.getDisplayValue());
                 me.collapse();
             }
-        }   
+        }
+    },
+
+    onBeforeSelect: function(list, record) {
+        return this.fireEvent('beforeselect', this, record, record.index);
+    },
+
+    onBeforeDeselect: function(list, record) {
+        return this.fireEvent('beforedeselect', this, record, record.index);
     },
 
     onListSelectionChange: function(list, selectedRecords) {
-        var me = this;
+        var me = this,
+            isMulti = me.multiSelect,
+            hasRecords = selectedRecords.length > 0;
         // Only react to selection if it is not called from setValue, and if our list is
         // expanded (ignores changes to the selection model triggered elsewhere)
         if (!me.ignoreSelection && me.isExpanded) {
-            if (!me.multiSelect) {
+            if (!isMulti) {
                 Ext.defer(me.collapse, 1, me);
             }
-            me.setValue(selectedRecords, false);
-            if (selectedRecords.length > 0) {
+            /*
+             * Only set the value here if we're in multi selection mode or we have
+             * a selection. Otherwise setValue will be called with an empty value
+             * which will cause the change event to fire twice.
+             */
+            if (isMulti || hasRecords) {
+                me.setValue(selectedRecords, false);
+            }
+            if (hasRecords) {
                 me.fireEvent('select', me, selectedRecords);
             }
             me.inputEl.focus();
@@ -850,25 +1039,38 @@ var combo = new Ext.form.field.ComboBox({
 
     /**
      * Selects an item by a {@link Ext.data.Model Model}, or by a key value.
-     * @param r
+     * @param {Object} r
      */
     select: function(r) {
         this.setValue(r, true);
     },
 
     /**
-     * Find the record by searching for a specific field/value combination
-     * Returns an Ext.data.Record or false
-     * @private
+     * Finds the record by searching for a specific field/value combination.
+     * @param {String} field The name of the field to test.
+     * @param {Object} value The value to match the field against.
+     * @return {Ext.data.Model} The matched record or false.
      */
     findRecord: function(field, value) {
         var ds = this.store,
             idx = ds.findExact(field, value);
         return idx !== -1 ? ds.getAt(idx) : false;
     },
+
+    /**
+     * Finds the record by searching values in the {@link #valueField}.
+     * @param {Object} value The value to match the field against.
+     * @return {Ext.data.Model} The matched record or false.
+     */
     findRecordByValue: function(value) {
         return this.findRecord(this.valueField, value);
     },
+
+    /**
+     * Finds the record by searching values in the {@link #displayField}.
+     * @param {Object} value The value to match the field against.
+     * @return {Ext.data.Model} The matched record or false.
+     */
     findRecordByDisplay: function(value) {
         return this.findRecord(this.displayField, value);
     },
@@ -876,9 +1078,9 @@ var combo = new Ext.form.field.ComboBox({
     /**
      * Sets the specified value(s) into the field. For each value, if a record is found in the {@link #store} that
      * matches based on the {@link #valueField}, then that record's {@link #displayField} will be displayed in the
-     * field.  If no match is found, and the {@link #valueNotFoundText} config option is defined, then that will be
+     * field. If no match is found, and the {@link #valueNotFoundText} config option is defined, then that will be
      * displayed as the default field text. Otherwise a blank value will be shown, although the value will still be set.
-     * @param {String|Array} value The value(s) to be set. Can be either a single String or {@link Ext.data.Model},
+     * @param {String/String[]} value The value(s) to be set. Can be either a single String or {@link Ext.data.Model},
      * or an Array of Strings or Models.
      * @return {Ext.form.field.Field} this
      */
@@ -894,6 +1096,7 @@ var combo = new Ext.form.field.ComboBox({
         if (me.store.loading) {
             // Called while the Store is loading. Ensure it is processed by the onLoad method.
             me.value = value;
+            me.setHiddenValue(me.value);
             return me;
         }
 
@@ -915,15 +1118,20 @@ var combo = new Ext.form.field.ComboBox({
             // record was not found, this could happen because
             // store is not loaded or they set a value not in the store
             else {
-                // if valueNotFoundText is defined, display it, otherwise display nothing for this value
-                if (Ext.isDefined(valueNotFoundText)) {
+                // If we are allowing insertion of values not represented in the Store, then set the value, and the display value
+                if (!me.forceSelection) {
+                    displayTplData.push(value[i]);
+                    processedValue.push(value[i]);
+                }
+                // Else, if valueNotFoundText is defined, display it, otherwise display nothing for this value
+                else if (Ext.isDefined(valueNotFoundText)) {
                     displayTplData.push(valueNotFoundText);
                 }
-                processedValue.push(value[i]);
             }
         }
 
         // Set the value of this field. If we are multiselecting, then that is an array.
+        me.setHiddenValue(processedValue);
         me.value = me.multiSelect ? processedValue : processedValue[0];
         if (!Ext.isDefined(me.value)) {
             me.value = null;
@@ -948,7 +1156,42 @@ var combo = new Ext.form.field.ComboBox({
     },
 
     /**
-     * @private Generate the string value to be displayed in the text field for the currently stored value
+     * @private
+     * Set the value of {@link #hiddenDataEl}
+     * Dynamically adds and removes input[type=hidden] elements
+     */
+    setHiddenValue: function(values){
+        var me = this, i;
+        if (!me.hiddenDataEl) {
+            return;
+        }
+        values = Ext.Array.from(values);
+        var dom = me.hiddenDataEl.dom,
+            childNodes = dom.childNodes,
+            input = childNodes[0],
+            valueCount = values.length,
+            childrenCount = childNodes.length;
+        
+        if (!input && valueCount > 0) {
+            me.hiddenDataEl.update(Ext.DomHelper.markup({tag:'input', type:'hidden', name:me.name}));
+            childrenCount = 1;
+            input = dom.firstChild;
+        }
+        while (childrenCount > valueCount) {
+            dom.removeChild(childNodes[0]);
+            -- childrenCount;
+        }
+        while (childrenCount < valueCount) {
+            dom.appendChild(input.cloneNode(true));
+            ++ childrenCount;
+        }
+        for (i = 0; i < valueCount; i++) {
+            childNodes[i].value = values[i];
+        }
+    },
+
+    /**
+     * @private Generates the string value to be displayed in the text field for the currently stored value
      */
     getDisplayValue: function() {
         return this.displayTpl.apply(this.displayTplData);
@@ -1036,3 +1279,4 @@ var combo = new Ext.form.field.ComboBox({
         }
     }
 });
+