3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
9 GNU General Public License Usage
10 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.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * @class Ext.form.field.ComboBox
17 * @extends Ext.form.field.Picker
19 * A combobox control with support for autocomplete, remote loading, and many other features.
21 * A ComboBox is like a combination of a traditional HTML text `<input>` field and a `<select>`
22 * field; the user is able to type freely into the field, and/or pick values from a dropdown selection
23 * list. The user can input any value by default, even if it does not appear in the selection list;
24 * to prevent free-form values and restrict them to items in the list, set {@link #forceSelection} to `true`.
26 * The selection list's options are populated from any {@link Ext.data.Store}, including remote
27 * stores. The data items in the store are mapped to each option's displayed text and backing value via
28 * the {@link #valueField} and {@link #displayField} configurations, respectively.
30 * If your store is not remote, i.e. it depends only on local data and is loaded up front, you should be
31 * sure to set the {@link #queryMode} to `'local'`, as this will improve responsiveness for the user.
33 * {@img Ext.form.ComboBox/Ext.form.ComboBox.png Ext.form.ComboBox component}
37 * // The data store containing the list of states
38 * var states = Ext.create('Ext.data.Store', {
39 * fields: ['abbr', 'name'],
41 * {"abbr":"AL", "name":"Alabama"},
42 * {"abbr":"AK", "name":"Alaska"},
43 * {"abbr":"AZ", "name":"Arizona"}
48 * // Create the combo box, attached to the states data store
49 * Ext.create('Ext.form.ComboBox', {
50 * fieldLabel: 'Choose State',
53 * displayField: 'name',
55 * renderTo: Ext.getBody()
60 * To do something when something in ComboBox is selected, configure the select event:
62 * var cb = Ext.create('Ext.form.ComboBox', {
63 * // all of your config options
66 * 'select': yourFunction
70 * // Alternatively, you can assign events after the object is created:
71 * var cb = new Ext.form.field.ComboBox(yourOptions);
72 * cb.on('select', yourFunction, yourScope);
74 * ## Multiple Selection
76 * ComboBox also allows selection of multiple items from the list; to enable multi-selection set the
77 * {@link #multiSelect} config to `true`.
79 * @docauthor Jason Johnston <jason@sencha.com>
81 Ext.define('Ext.form.field.ComboBox', {
82 extend:'Ext.form.field.Picker',
83 requires: ['Ext.util.DelayedTask', 'Ext.EventObject', 'Ext.view.BoundList', 'Ext.view.BoundListKeyNav', 'Ext.data.StoreManager'],
84 alternateClassName: 'Ext.form.ComboBox',
85 alias: ['widget.combobox', 'widget.combo'],
88 * @cfg {String} triggerCls
89 * An additional CSS class used to style the trigger button. The trigger will always get the
90 * {@link #triggerBaseCls} by default and <code>triggerCls</code> will be <b>appended</b> if specified.
91 * Defaults to 'x-form-arrow-trigger' for ComboBox.
93 triggerCls: Ext.baseCSSPrefix + 'form-arrow-trigger',
96 * @cfg {Ext.data.Store/Array} store The data source to which this combo is bound (defaults to <code>undefined</code>).
97 * Acceptable values for this property are:
98 * <div class="mdetail-params"><ul>
99 * <li><b>any {@link Ext.data.Store Store} subclass</b></li>
100 * <li><b>an Array</b> : Arrays will be converted to a {@link Ext.data.Store} internally,
101 * automatically generating {@link Ext.data.Field#name field names} to work with all data components.
102 * <div class="mdetail-params"><ul>
103 * <li><b>1-dimensional array</b> : (e.g., <code>['Foo','Bar']</code>)<div class="sub-desc">
104 * A 1-dimensional array will automatically be expanded (each array item will be used for both the combo
105 * {@link #valueField} and {@link #displayField})</div></li>
106 * <li><b>2-dimensional array</b> : (e.g., <code>[['f','Foo'],['b','Bar']]</code>)<div class="sub-desc">
107 * For a multi-dimensional array, the value in index 0 of each item will be assumed to be the combo
108 * {@link #valueField}, while the value at index 1 is assumed to be the combo {@link #displayField}.
109 * </div></li></ul></div></li></ul></div>
110 * <p>See also <code>{@link #queryMode}</code>.</p>
114 * @cfg {Boolean} multiSelect
115 * If set to <code>true</code>, allows the combo field to hold more than one value at a time, and allows selecting
116 * multiple items from the dropdown list. The combo's text field will show all selected values separated by
117 * the {@link #delimiter}. (Defaults to <code>false</code>.)
122 * @cfg {String} delimiter
123 * The character(s) used to separate the {@link #displayField display values} of multiple selected items
124 * when <code>{@link #multiSelect} = true</code>. Defaults to <code>', '</code>.
129 * @cfg {String} displayField The underlying {@link Ext.data.Field#name data field name} to bind to this
130 * ComboBox (defaults to 'text').
131 * <p>See also <code>{@link #valueField}</code>.</p>
133 displayField: 'text',
136 * @cfg {String} valueField
138 * The underlying {@link Ext.data.Field#name data value name} to bind to this ComboBox (defaults to match
139 * the value of the {@link #displayField} config).
140 * <p><b>Note</b>: use of a <code>valueField</code> requires the user to make a selection in order for a value to be
141 * mapped. See also <code>{@link #displayField}</code>.</p>
145 * @cfg {String} triggerAction The action to execute when the trigger is clicked.
146 * <div class="mdetail-params"><ul>
147 * <li><b><code>'all'</code></b> : <b>Default</b>
148 * <p class="sub-desc">{@link #doQuery run the query} specified by the <code>{@link #allQuery}</code> config option</p></li>
149 * <li><b><code>'query'</code></b> :
150 * <p class="sub-desc">{@link #doQuery run the query} using the {@link Ext.form.field.Base#getRawValue raw value}.</p></li>
152 * <p>See also <code>{@link #queryParam}</code>.</p>
154 triggerAction: 'all',
157 * @cfg {String} allQuery The text query to send to the server to return all records for the list
158 * with no filtering (defaults to '')
163 * @cfg {String} queryParam Name of the parameter used by the Store to pass the typed string when the ComboBox is configured with
164 * <code>{@link #queryMode}: 'remote'</code> (defaults to <code>'query'</code>). If explicitly set to a falsy value it will
170 * @cfg {String} queryMode
171 * The mode in which the ComboBox uses the configured Store. Acceptable values are:
172 * <div class="mdetail-params"><ul>
173 * <li><b><code>'remote'</code></b> : <b>Default</b>
174 * <p>In <code>queryMode: 'remote'</code>, the ComboBox loads its Store dynamically based upon user interaction.</p>
175 * <p>This is typically used for "autocomplete" type inputs, and after the user finishes typing, the Store is {@link Ext.data.Store#load load}ed.</p>
176 * <p>A parameter containing the typed string is sent in the load request. The default parameter name for the input string is <code>query</code>, but this
177 * can be configured using the {@link #queryParam} config.</p>
178 * <p>In <code>queryMode: 'remote'</code>, the Store may be configured with <code>{@link Ext.data.Store#remoteFilter remoteFilter}: true</code>,
179 * and further filters may be <i>programatically</i> added to the Store which are then passed with every load request which allows the server
180 * to further refine the returned dataset.</p>
181 * <p>Typically, in an autocomplete situation, {@link #hideTrigger} is configured <code>true</code> because it has no meaning for autocomplete.</p></li>
182 * <li><b><code>'local'</code></b> :
183 * <p class="sub-desc">ComboBox loads local data</p>
185 var combo = new Ext.form.field.ComboBox({
186 renderTo: document.body,
188 store: new Ext.data.ArrayStore({
191 'myId', // numeric value is the key
194 data: [[1, 'item1'], [2, 'item2']] // data is local
197 displayField: 'displayText',
208 * @cfg {Number} pageSize If greater than <code>0</code>, a {@link Ext.toolbar.Paging} is displayed in the
209 * footer of the dropdown list and the {@link #doQuery filter queries} will execute with page start and
210 * {@link Ext.toolbar.Paging#pageSize limit} parameters. Only applies when <code>{@link #queryMode} = 'remote'</code>
211 * (defaults to <code>0</code>).
216 * @cfg {Number} queryDelay The length of time in milliseconds to delay between the start of typing and
217 * sending the query to filter the dropdown list (defaults to <code>500</code> if <code>{@link #queryMode} = 'remote'</code>
218 * or <code>10</code> if <code>{@link #queryMode} = 'local'</code>)
222 * @cfg {Number} minChars The minimum number of characters the user must type before autocomplete and
223 * {@link #typeAhead} activate (defaults to <code>4</code> if <code>{@link #queryMode} = 'remote'</code> or <code>0</code> if
224 * <code>{@link #queryMode} = 'local'</code>, does not apply if <code>{@link Ext.form.field.Trigger#editable editable} = false</code>).
228 * @cfg {Boolean} autoSelect <code>true</code> to automatically highlight the first result gathered by the data store
229 * in the dropdown list when it is opened. (Defaults to <code>true</code>). A false value would cause nothing in the
230 * list to be highlighted automatically, so the user would have to manually highlight an item before pressing
231 * the enter or {@link #selectOnTab tab} key to select it (unless the value of ({@link #typeAhead}) were true),
232 * or use the mouse to select a value.
237 * @cfg {Boolean} typeAhead <code>true</code> to populate and autoselect the remainder of the text being
238 * typed after a configurable delay ({@link #typeAheadDelay}) if it matches a known value (defaults
239 * to <code>false</code>)
244 * @cfg {Number} typeAheadDelay The length of time in milliseconds to wait until the typeahead text is displayed
245 * if <code>{@link #typeAhead} = true</code> (defaults to <code>250</code>)
250 * @cfg {Boolean} selectOnTab
251 * Whether the Tab key should select the currently highlighted item. Defaults to <code>true</code>.
256 * @cfg {Boolean} forceSelection <code>true</code> to restrict the selected value to one of the values in the list,
257 * <code>false</code> to allow the user to set arbitrary text into the field (defaults to <code>false</code>)
259 forceSelection: false,
262 * @cfg {String} valueNotFoundText When using a name/value combo, if the value passed to setValue is not found in
263 * the store, valueNotFoundText will be displayed as the field text if defined (defaults to undefined). If this
264 * default text is used, it means there is no value set and no validation will occur on this field.
268 * The value of the match string used to filter the store. Delete this property to force a requery.
271 var combo = new Ext.form.field.ComboBox({
275 // delete the previous query in the beforequery event or set
276 // combo.lastQuery = null (this will reload the store the next time it expands)
277 beforequery: function(qe){
278 delete qe.combo.lastQuery;
283 * To make sure the filter in the store is not cleared the first time the ComboBox trigger is used
284 * configure the combo with <code>lastQuery=''</code>. Example use:
286 var combo = new Ext.form.field.ComboBox({
289 triggerAction: 'all',
293 * @property lastQuery
298 * @cfg {Object} defaultListConfig
299 * Set of options that will be used as defaults for the user-configured {@link #listConfig} object.
303 loadingText: 'Loading...',
311 * @cfg {Mixed} transform
312 * The id, DOM node or {@link Ext.core.Element} of an existing HTML <code><select></code> element to
313 * convert into a ComboBox. The target select's options will be used to build the options in the ComboBox
314 * dropdown; a configured {@link #store} will take precedence over this.
318 * @cfg {Object} listConfig
319 * <p>An optional set of configuration properties that will be passed to the {@link Ext.view.BoundList}'s
320 * constructor. Any configuration that is valid for BoundList can be included. Some of the more useful
323 * <li>{@link Ext.view.BoundList#cls} - defaults to empty</li>
324 * <li>{@link Ext.view.BoundList#emptyText} - defaults to empty string</li>
325 * <li>{@link Ext.view.BoundList#getInnerTpl} - defaults to the template defined in BoundList</li>
326 * <li>{@link Ext.view.BoundList#itemSelector} - defaults to the value defined in BoundList</li>
327 * <li>{@link Ext.view.BoundList#loadingText} - defaults to <code>'Loading...'</code></li>
328 * <li>{@link Ext.view.BoundList#minWidth} - defaults to <code>70</code></li>
329 * <li>{@link Ext.view.BoundList#maxWidth} - defaults to <code>undefined</code></li>
330 * <li>{@link Ext.view.BoundList#maxHeight} - defaults to <code>300</code></li>
331 * <li>{@link Ext.view.BoundList#resizable} - defaults to <code>false</code></li>
332 * <li>{@link Ext.view.BoundList#shadow} - defaults to <code>'sides'</code></li>
333 * <li>{@link Ext.view.BoundList#width} - defaults to <code>undefined</code> (automatically set to the width
334 * of the ComboBox field if {@link #matchFieldWidth} is true)</li>
341 initComponent: function() {
343 isDefined = Ext.isDefined,
345 transform = me.transform,
346 transformSelect, isLocalMode;
349 if (!store && !transform) {
350 Ext.Error.raise('Either a valid store, or a HTML select to transform, must be configured on the combo.');
352 if (me.typeAhead && me.multiSelect) {
353 Ext.Error.raise('typeAhead and multiSelect are mutually exclusive options -- please remove one of them.');
355 if (me.typeAhead && !me.editable) {
356 Ext.Error.raise('If typeAhead is enabled the combo must be editable: true -- please change one of those settings.');
358 if (me.selectOnFocus && !me.editable) {
359 Ext.Error.raise('If selectOnFocus is enabled the combo must be editable: true -- please change one of those settings.');
364 // TODO need beforeselect?
368 * Fires before all queries are processed. Return false to cancel the query or set the queryEvent's
369 * cancel property to true.
370 * @param {Object} queryEvent An object that has these properties:<ul>
371 * <li><code>combo</code> : Ext.form.field.ComboBox <div class="sub-desc">This combo box</div></li>
372 * <li><code>query</code> : String <div class="sub-desc">The query string</div></li>
373 * <li><code>forceAll</code> : Boolean <div class="sub-desc">True to force "all" query</div></li>
374 * <li><code>cancel</code> : Boolean <div class="sub-desc">Set to true to cancel the query</div></li>
381 * Fires when at least one list item is selected.
382 * @param {Ext.form.field.ComboBox} combo This combo box
383 * @param {Array} records The selected records
388 // Build store from 'transform' HTML select element's options
389 if (!store && transform) {
390 transformSelect = Ext.getDom(transform);
391 if (transformSelect) {
392 store = Ext.Array.map(Ext.Array.from(transformSelect.options), function(option) {
393 return [option.value, option.text];
396 me.name = transformSelect.name;
398 if (!('value' in me)) {
399 me.value = transformSelect.value;
404 me.bindStore(store, true);
406 if (store.autoCreated) {
407 me.queryMode = 'local';
408 me.valueField = me.displayField = 'field1';
409 if (!store.expanded) {
410 me.displayField = 'field2';
415 if (!isDefined(me.valueField)) {
416 me.valueField = me.displayField;
419 isLocalMode = me.queryMode === 'local';
420 if (!isDefined(me.queryDelay)) {
421 me.queryDelay = isLocalMode ? 10 : 500;
423 if (!isDefined(me.minChars)) {
424 me.minChars = isLocalMode ? 0 : 4;
427 if (!me.displayTpl) {
428 me.displayTpl = Ext.create('Ext.XTemplate',
430 '{[typeof values === "string" ? values : values.' + me.displayField + ']}' +
431 '<tpl if="xindex < xcount">' + me.delimiter + '</tpl>' +
434 } else if (Ext.isString(me.displayTpl)) {
435 me.displayTpl = Ext.create('Ext.XTemplate', me.displayTpl);
440 me.doQueryTask = Ext.create('Ext.util.DelayedTask', me.doRawQuery, me);
442 // store has already been loaded, setValue
443 if (me.store.getCount() > 0) {
444 me.setValue(me.value);
447 // render in place of 'transform' select
448 if (transformSelect) {
449 me.render(transformSelect.parentNode, transformSelect);
450 Ext.removeNode(transformSelect);
455 beforeBlur: function() {
457 me.doQueryTask.cancel();
458 if (me.forceSelection) {
466 assertValue: function() {
468 value = me.getRawValue(),
471 if (me.multiSelect) {
472 // For multiselect, check that the current displayed value matches the current
473 // selection, if it does not then revert to the most recent selection.
474 if (value !== me.getDisplayValue()) {
475 me.setValue(me.lastSelection);
478 // For single-select, match the displayed value to a record and select it,
479 // if it does not match a record then revert to the most recent selection.
480 rec = me.findRecordByDisplay(value);
484 me.setValue(me.lastSelection);
490 onTypeAhead: function() {
492 displayField = me.displayField,
493 record = me.store.findRecord(displayField, me.getRawValue()),
494 boundList = me.getPicker(),
495 newValue, len, selStart;
498 newValue = record.get(displayField);
499 len = newValue.length;
500 selStart = me.getRawValue().length;
502 boundList.highlightItem(boundList.getNode(record));
504 if (selStart !== 0 && selStart !== len) {
505 me.setRawValue(newValue);
506 me.selectText(selStart, newValue.length);
511 // invoked when a different store is bound to this combo
513 resetToDefault: function() {
517 bindStore: function(store, initial) {
521 // this code directly accesses this.picker, bc invoking getPicker
522 // would create it when we may be preping to destroy it
523 if (oldStore && !initial) {
524 if (oldStore !== store && oldStore.autoDestroy) {
530 exception: me.collapse
536 me.picker.bindStore(null);
545 me.store = Ext.data.StoreManager.lookup(store);
549 exception: me.collapse
553 me.picker.bindStore(store);
563 if (me.picker && !me.picker.getSelectionModel().hasSelection()) {
570 * Execute the query with the raw contents within the textfield.
572 doRawQuery: function() {
573 this.doQuery(this.getRawValue());
577 * Executes a query to filter the dropdown list. Fires the {@link #beforequery} event prior to performing the
578 * query allowing the query action to be canceled if needed.
579 * @param {String} queryString The SQL query to execute
580 * @param {Boolean} forceAll <code>true</code> to force the query to execute even if there are currently fewer
581 * characters in the field than the minimum specified by the <code>{@link #minChars}</code> config option. It
582 * also clears any filter previously saved in the current store (defaults to <code>false</code>)
583 * @return {Boolean} true if the query was permitted to run, false if it was cancelled by a {@link #beforequery} handler.
585 doQuery: function(queryString, forceAll) {
586 queryString = queryString || '';
588 // store in object and pass by reference in 'beforequery'
589 // so that client code can modify values.
598 isLocalMode = me.queryMode === 'local';
600 if (me.fireEvent('beforequery', qe) === false || qe.cancel) {
604 // get back out possibly modified values
605 queryString = qe.query;
606 forceAll = qe.forceAll;
608 // query permitted to run
609 if (forceAll || (queryString.length >= me.minChars)) {
610 // expand before starting query so LoadMask can position itself correctly
613 // make sure they aren't querying the same thing
614 if (!me.queryCaching || me.lastQuery !== queryString) {
615 me.lastQuery = queryString;
618 // forceAll means no filtering - show whole dataset.
622 // Clear filter, but supress event so that the BoundList is not immediately updated.
623 store.clearFilter(true);
624 store.filter(me.displayField, queryString);
627 // In queryMode: 'remote', we assume Store filters are added by the developer as remote filters,
628 // and these are automatically passed as params with every load call, so we do *not* call clearFilter.
630 params: me.getParams(queryString)
635 // Clear current selection if it does not match the current value in the field
636 if (me.getRawValue() !== me.getDisplayValue()) {
637 me.ignoreSelection++;
638 me.picker.getSelectionModel().deselectAll();
639 me.ignoreSelection--;
653 getParams: function(queryString) {
655 pageSize = this.pageSize,
656 param = this.queryParam;
659 p[param] = queryString;
671 * If the autoSelect config is true, and the picker is open, highlights the first item.
673 doAutoSelect: function() {
676 lastSelected, itemNode;
677 if (picker && me.autoSelect && me.store.getCount() > 0) {
678 // Highlight the last selected item and scroll it into view
679 lastSelected = picker.getSelectionModel().lastSelected;
680 itemNode = picker.getNode(lastSelected || 0);
682 picker.highlightItem(itemNode);
683 picker.listEl.scrollChildIntoView(itemNode, false);
688 doTypeAhead: function() {
689 if (!this.typeAheadTask) {
690 this.typeAheadTask = Ext.create('Ext.util.DelayedTask', this.onTypeAhead, this);
692 if (this.lastKey != Ext.EventObject.BACKSPACE && this.lastKey != Ext.EventObject.DELETE) {
693 this.typeAheadTask.delay(this.typeAheadDelay);
697 onTriggerClick: function() {
699 if (!me.readOnly && !me.disabled) {
704 if (me.triggerAction === 'all') {
705 me.doQuery(me.allQuery, true);
707 me.doQuery(me.getRawValue());
715 // store the last key and doQuery if relevant
716 onKeyUp: function(e, t) {
720 if (!me.readOnly && !me.disabled && me.editable) {
722 // we put this in a task so that we can cancel it if a user is
723 // in and out before the queryDelay elapses
725 // perform query w/ any normal key or backspace or delete
726 if (!e.isSpecialKey() || key == e.BACKSPACE || key == e.DELETE) {
727 me.doQueryTask.delay(me.queryDelay);
731 if (me.enableKeyEvents) {
732 me.callParent(arguments);
736 initEvents: function() {
741 * Setup keyboard handling. If enableKeyEvents is true, we already have
742 * a listener on the inputEl for keyup, so don't create a second.
744 if (!me.enableKeyEvents) {
745 me.mon(me.inputEl, 'keyup', me.onKeyUp, me);
749 createPicker: function() {
752 menuCls = Ext.baseCSSPrefix + 'menu',
755 mode: me.multiSelect ? 'SIMPLE' : 'SINGLE'
760 cls: me.el.up('.' + menuCls) ? menuCls : '',
762 displayField: me.displayField,
763 focusOnToFront: false,
764 pageSize: me.pageSize,
766 }, me.listConfig, me.defaultListConfig);
768 picker = me.picker = Ext.create('Ext.view.BoundList', opts);
771 itemclick: me.onItemClick,
772 refresh: me.onListRefresh,
776 me.mon(picker.getSelectionModel(), 'selectionchange', me.onListSelectionChange, me);
781 onListRefresh: function() {
783 this.syncSelection();
786 onItemClick: function(picker, record){
788 * If we're doing single selection, the selection change events won't fire when
789 * clicking on the selected element. Detect it here.
792 lastSelection = me.lastSelection,
793 valueField = me.valueField,
796 if (!me.multiSelect && lastSelection) {
797 selected = lastSelection[0];
798 if (selected && (record.get(valueField) === selected.get(valueField))) {
804 onListSelectionChange: function(list, selectedRecords) {
806 isMulti = me.multiSelect,
807 hasRecords = selectedRecords.length > 0;
808 // Only react to selection if it is not called from setValue, and if our list is
809 // expanded (ignores changes to the selection model triggered elsewhere)
810 if (!me.ignoreSelection && me.isExpanded) {
812 Ext.defer(me.collapse, 1, me);
815 * Only set the value here if we're in multi selection mode or we have
816 * a selection. Otherwise setValue will be called with an empty value
817 * which will cause the change event to fire twice.
819 if (isMulti || hasRecords) {
820 me.setValue(selectedRecords, false);
823 me.fireEvent('select', me, selectedRecords);
831 * Enables the key nav for the BoundList when it is expanded.
833 onExpand: function() {
835 keyNav = me.listKeyNav,
836 selectOnTab = me.selectOnTab,
837 picker = me.getPicker();
839 // Handle BoundList navigation from the input field. Insert a tab listener specially to enable selectOnTab.
843 keyNav = me.listKeyNav = Ext.create('Ext.view.BoundListKeyNav', this.inputEl, {
848 this.selectHighlighted(e);
851 // Tab key event is allowed to propagate to field
857 // While list is expanded, stop tab monitoring from Ext.form.field.Trigger so it doesn't short-circuit selectOnTab
859 me.ignoreMonitorTab = true;
862 Ext.defer(keyNav.enable, 1, keyNav); //wait a bit so it doesn't react to the down arrow opening the picker
868 * Disables the key nav for the BoundList when it is collapsed.
870 onCollapse: function() {
872 keyNav = me.listKeyNav;
875 me.ignoreMonitorTab = false;
880 * Selects an item by a {@link Ext.data.Model Model}, or by a key value.
883 select: function(r) {
884 this.setValue(r, true);
888 * Find the record by searching for a specific field/value combination
889 * Returns an Ext.data.Record or false
892 findRecord: function(field, value) {
894 idx = ds.findExact(field, value);
895 return idx !== -1 ? ds.getAt(idx) : false;
897 findRecordByValue: function(value) {
898 return this.findRecord(this.valueField, value);
900 findRecordByDisplay: function(value) {
901 return this.findRecord(this.displayField, value);
905 * Sets the specified value(s) into the field. For each value, if a record is found in the {@link #store} that
906 * matches based on the {@link #valueField}, then that record's {@link #displayField} will be displayed in the
907 * field. If no match is found, and the {@link #valueNotFoundText} config option is defined, then that will be
908 * displayed as the default field text. Otherwise a blank value will be shown, although the value will still be set.
909 * @param {String|Array} value The value(s) to be set. Can be either a single String or {@link Ext.data.Model},
910 * or an Array of Strings or Models.
911 * @return {Ext.form.field.Field} this
913 setValue: function(value, doSelect) {
915 valueNotFoundText = me.valueNotFoundText,
916 inputEl = me.inputEl,
922 if (me.store.loading) {
923 // Called while the Store is loading. Ensure it is processed by the onLoad method.
928 // This method processes multi-values, so ensure value is an array.
929 value = Ext.Array.from(value);
931 // Loop through values
932 for (i = 0, len = value.length; i < len; i++) {
934 if (!record || !record.isModel) {
935 record = me.findRecordByValue(record);
937 // record found, select it.
940 displayTplData.push(record.data);
941 processedValue.push(record.get(me.valueField));
943 // record was not found, this could happen because
944 // store is not loaded or they set a value not in the store
946 // if valueNotFoundText is defined, display it, otherwise display nothing for this value
947 if (Ext.isDefined(valueNotFoundText)) {
948 displayTplData.push(valueNotFoundText);
950 processedValue.push(value[i]);
954 // Set the value of this field. If we are multiselecting, then that is an array.
955 me.value = me.multiSelect ? processedValue : processedValue[0];
956 if (!Ext.isDefined(me.value)) {
959 me.displayTplData = displayTplData; //store for getDisplayValue method
960 me.lastSelection = me.valueModels = models;
962 if (inputEl && me.emptyText && !Ext.isEmpty(value)) {
963 inputEl.removeCls(me.emptyCls);
966 // Calculate raw value from the collection of Model data
967 me.setRawValue(me.getDisplayValue());
970 if (doSelect !== false) {
979 * @private Generate the string value to be displayed in the text field for the currently stored value
981 getDisplayValue: function() {
982 return this.displayTpl.apply(this.displayTplData);
985 getValue: function() {
986 // If the user has not changed the raw field value since a value was selected from the list,
987 // then return the structured value from the selection. If the raw field value is different
988 // than what would be displayed due to selection, return that raw value.
991 rawValue = me.getRawValue(), //current value of text field
992 value = me.value; //stored value from last selection or setValue() call
994 if (me.getDisplayValue() !== rawValue) {
996 me.value = me.displayTplData = me.valueModels = null;
998 me.ignoreSelection++;
999 picker.getSelectionModel().deselectAll();
1000 me.ignoreSelection--;
1007 getSubmitValue: function() {
1008 return this.getValue();
1011 isEqual: function(v1, v2) {
1012 var fromArray = Ext.Array.from,
1019 if (len !== v2.length) {
1023 for(i = 0; i < len; i++) {
1024 if (v2[i] !== v1[i]) {
1033 * Clears any value currently set in the ComboBox.
1035 clearValue: function() {
1040 * @private Synchronizes the selection in the picker to match the current value of the combobox.
1042 syncSelection: function() {
1044 ExtArray = Ext.Array,
1046 selection, selModel;
1048 // From the value, find the Models that are in the store's current data
1050 ExtArray.forEach(me.valueModels || [], function(value) {
1051 if (value && value.isModel && me.store.indexOf(value) >= 0) {
1052 selection.push(value);
1056 // Update the selection to match
1057 me.ignoreSelection++;
1058 selModel = picker.getSelectionModel();
1059 selModel.deselectAll();
1060 if (selection.length) {
1061 selModel.select(selection);
1063 me.ignoreSelection--;