Combo

Combo (dropdown) widget. Consists of a text field with a trigger icon, which displays a List. Can be populated from a Store.

Combo
//<code-header>
fiddle.title = 'Combo';
//</code-header>
new Combo({
    items         : ['Fanta', 'Loranga', 'Jaffa', 'Zingo', 'Orangina'],
    label         : 'Soda',
    labelPosition : 'above',
    placeholder   : 'Select a soda',
    appendTo      : targetElement
});

Please be aware that when populating the Combo with objects or records, you have to configure valueField and displayField to point to the correct field names in your data.

This field can be used as an editor for the Column.

Basic scenarios

Basic combo
//<code-header>
fiddle.title = 'Basic combo';
//</code-header>
// combo with string items
new Combo({
    labelPosition : 'above',
    items         : ['Fanta', 'Loranga', 'Jaffa', 'Zingo', 'Orangina'],
    label         : 'Items as strings',
    appendTo      : targetElement
});

// combo with object items
new Combo({
    labelPosition : 'above',
    items         : [{ value : 'pepsi', text : 'Pepsi' }, { value : 'coke', text : 'Coca Cola' }],
    label         : 'Items as objects',
    appendTo      : targetElement
});

// non-editable combo (user can only pick from list)
new Combo({
    labelPosition : 'above',
    items         : [{ value : 'MtnDew', text : 'Mountain Dew' }, 'Sprite', '7up'],
    label         : 'Not editable',
    editable      : false,
    appendTo      : targetElement
});

// editable combo (user can type to filter)
new Combo({
    labelPosition : 'above',
    items         : ['Captain America', 'Hulk', 'She-Hulk', 'Hawkeye'],
    label         : 'Editable',
    editable      : true,
    appendTo      : targetElement
});

Loading data from simple string array:

Filtering on typing will still work when non-editable. The characters typed are collected and used to filter the list of items in the picker. The typed value expires after 2 seconds of inactivity. This is useful when a non-editable Combo has a very large dataset and you want to filter it by typing.

The combo's configured primaryFilter is used which uses the startsWith operator by default.

Basic scenarios

Combo
//<code-header>
fiddle.title = 'Combo';
//</code-header>
new Combo({
    items         : ['Fanta', 'Loranga', 'Jaffa', 'Zingo', 'Orangina'],
    label         : 'Soda',
    labelPosition : 'above',
    placeholder   : 'Select a soda',
    appendTo      : targetElement
});

Strict picker width matching

By default, a Combo's picker, auto widths around its visible content, but will be at least the width of the Combo's input area. See the first example above.

To enforce strict width matching of the picker to the input area, configure the combo's picker like this

const combo = new Combo({
    items       : ['Small', 'Mediunm', 'Large', 'Ridiculously huge which would cause a very wide dropdown'],
    placeholder : 'Pick size of diamond for ring',
    picker : {
        align : {
            // Override default which is 'min'
            matchSize : true
        }
    }
});

Autocomplete / Type ahead

Implementing remote type-ahead functionality is simple, configure a store with readUrl and define the minChars required to type before remote loading is triggered.

Remote combo
//<code-header>
fiddle.title = 'Remote combo';
//</code-header>
// Type-ahead combo list with remotely loaded data
new Combo({
    appendTo        : targetElement,
    label           : 'Countries',
    minChars        : 2,
    displayField    : 'name',
    valueField      : 'code',
    filterParamName : 'filter',
    store           : {
        readUrl : 'remotecombo'
    }
});

const countries = [
    { name : 'Afghanistan', code : 'AF' },
    { name : 'Åland Islands', code : 'AX' },
    { name : 'Albania', code : 'AL' },
    { name : 'Algeria', code : 'DZ' },
    { name : 'American Samoa', code : 'AS' },
    { name : 'AndorrA', code : 'AD' },
    { name : 'Angola', code : 'AO' },
    { name : 'Anguilla', code : 'AI' },
    { name : 'Antarctica', code : 'AQ' },
    { name : 'Antigua and Barbuda', code : 'AG' },
    { name : 'Argentina', code : 'AR' },
    { name : 'Armenia', code : 'AM' },
    { name : 'Aruba', code : 'AW' },
    { name : 'Australia', code : 'AU' },
    { name : 'Austria', code : 'AT' },
    { name : 'Azerbaijan', code : 'AZ' },
    { name : 'Bahamas', code : 'BS' },
    { name : 'Bahrain', code : 'BH' },
    { name : 'Bangladesh', code : 'BD' },
    { name : 'Barbados', code : 'BB' },
    { name : 'Belarus', code : 'BY' },
    { name : 'Belgium', code : 'BE' },
    { name : 'Belize', code : 'BZ' },
    { name : 'Benin', code : 'BJ' },
    { name : 'Bermuda', code : 'BM' },
    { name : 'Bhutan', code : 'BT' },
    { name : 'Bolivia', code : 'BO' },
    { name : 'Bosnia and Herzegovina', code : 'BA' },
    { name : 'Botswana', code : 'BW' },
    { name : 'Bouvet Island', code : 'BV' },
    { name : 'Brazil', code : 'BR' },
    { name : 'British Indian Ocean Territory', code : 'IO' },
    { name : 'Brunei Darussalam', code : 'BN' },
    { name : 'Bulgaria', code : 'BG' },
    { name : 'Burkina Faso', code : 'BF' },
    { name : 'Burundi', code : 'BI' },
    { name : 'Cambodia', code : 'KH' },
    { name : 'Cameroon', code : 'CM' },
    { name : 'Canada', code : 'CA' },
    { name : 'Cape Verde', code : 'CV' },
    { name : 'Cayman Islands', code : 'KY' },
    { name : 'Central African Republic', code : 'CF' },
    { name : 'Chad', code : 'TD' },
    { name : 'Chile', code : 'CL' },
    { name : 'China', code : 'CN' },
    { name : 'Christmas Island', code : 'CX' },
    { name : 'Cocos (Keeling) Islands', code : 'CC' },
    { name : 'Colombia', code : 'CO' },
    { name : 'Comoros', code : 'KM' },
    { name : 'Congo', code : 'CG' },
    { name : 'Congo, The Democratic Republic of the', code : 'CD' },
    { name : 'Cook Islands', code : 'CK' },
    { name : 'Costa Rica', code : 'CR' },
    { name : 'Cote D\'Ivoire', code : 'CI' },
    { name : 'Croatia', code : 'HR' },
    { name : 'Cuba', code : 'CU' },
    { name : 'Cyprus', code : 'CY' },
    { name : 'Czech Republic', code : 'CZ' },
    { name : 'Denmark', code : 'DK' },
    { name : 'Djibouti', code : 'DJ' },
    { name : 'Dominica', code : 'DM' },
    { name : 'Dominican Republic', code : 'DO' },
    { name : 'Ecuador', code : 'EC' },
    { name : 'Egypt', code : 'EG' },
    { name : 'El Salvador', code : 'SV' },
    { name : 'Equatorial Guinea', code : 'GQ' },
    { name : 'Eritrea', code : 'ER' },
    { name : 'Estonia', code : 'EE' },
    { name : 'Ethiopia', code : 'ET' },
    { name : 'Falkland Islands (Malvinas)', code : 'FK' },
    { name : 'Faroe Islands', code : 'FO' },
    { name : 'Fiji', code : 'FJ' },
    { name : 'Finland', code : 'FI' },
    { name : 'France', code : 'FR' },
    { name : 'French Guiana', code : 'GF' },
    { name : 'French Polynesia', code : 'PF' },
    { name : 'French Southern Territories', code : 'TF' },
    { name : 'Gabon', code : 'GA' },
    { name : 'Gambia', code : 'GM' },
    { name : 'Georgia', code : 'GE' },
    { name : 'Germany', code : 'DE' },
    { name : 'Ghana', code : 'GH' },
    { name : 'Gibraltar', code : 'GI' },
    { name : 'Greece', code : 'GR' },
    { name : 'Greenland', code : 'GL' },
    { name : 'Grenada', code : 'GD' },
    { name : 'Guadeloupe', code : 'GP' },
    { name : 'Guam', code : 'GU' },
    { name : 'Guatemala', code : 'GT' },
    { name : 'Guernsey', code : 'GG' },
    { name : 'Guinea', code : 'GN' },
    { name : 'Guinea-Bissau', code : 'GW' },
    { name : 'Guyana', code : 'GY' },
    { name : 'Haiti', code : 'HT' },
    { name : 'Heard Island and Mcdonald Islands', code : 'HM' },
    { name : 'Holy See (Vatican City State)', code : 'VA' },
    { name : 'Honduras', code : 'HN' },
    { name : 'Hong Kong', code : 'HK' },
    { name : 'Hungary', code : 'HU' },
    { name : 'Iceland', code : 'IS' },
    { name : 'India', code : 'IN' },
    { name : 'Indonesia', code : 'ID' },
    { name : 'Iran, Islamic Republic Of', code : 'IR' },
    { name : 'Iraq', code : 'IQ' },
    { name : 'Ireland', code : 'IE' },
    { name : 'Isle of Man', code : 'IM' },
    { name : 'Israel', code : 'IL' },
    { name : 'Italy', code : 'IT' },
    { name : 'Jamaica', code : 'JM' },
    { name : 'Japan', code : 'JP' },
    { name : 'Jersey', code : 'JE' },
    { name : 'Jordan', code : 'JO' },
    { name : 'Kazakhstan', code : 'KZ' },
    { name : 'Kenya', code : 'KE' },
    { name : 'Kiribati', code : 'KI' },
    { name : 'Korea, Democratic People\'S Republic of', code : 'KP' },
    { name : 'Korea, Republic of', code : 'KR' },
    { name : 'Kuwait', code : 'KW' },
    { name : 'Kyrgyzstan', code : 'KG' },
    { name : 'Lao People\'S Democratic Republic', code : 'LA' },
    { name : 'Latvia', code : 'LV' },
    { name : 'Lebanon', code : 'LB' },
    { name : 'Lesotho', code : 'LS' },
    { name : 'Liberia', code : 'LR' },
    { name : 'Libyan Arab Jamahiriya', code : 'LY' },
    { name : 'Liechtenstein', code : 'LI' },
    { name : 'Lithuania', code : 'LT' },
    { name : 'Luxembourg', code : 'LU' },
    { name : 'Macao', code : 'MO' },
    { name : 'Macedonia, The Former Yugoslav Republic of', code : 'MK' },
    { name : 'Madagascar', code : 'MG' },
    { name : 'Malawi', code : 'MW' },
    { name : 'Malaysia', code : 'MY' },
    { name : 'Maldives', code : 'MV' },
    { name : 'Mali', code : 'ML' },
    { name : 'Malta', code : 'MT' },
    { name : 'Marshall Islands', code : 'MH' },
    { name : 'Martinique', code : 'MQ' },
    { name : 'Mauritania', code : 'MR' },
    { name : 'Mauritius', code : 'MU' },
    { name : 'Mayotte', code : 'YT' },
    { name : 'Mexico', code : 'MX' },
    { name : 'Micronesia, Federated States of', code : 'FM' },
    { name : 'Moldova, Republic of', code : 'MD' },
    { name : 'Monaco', code : 'MC' },
    { name : 'Mongolia', code : 'MN' },
    { name : 'Montserrat', code : 'MS' },
    { name : 'Morocco', code : 'MA' },
    { name : 'Mozambique', code : 'MZ' },
    { name : 'Myanmar', code : 'MM' },
    { name : 'Namibia', code : 'NA' },
    { name : 'Nauru', code : 'NR' },
    { name : 'Nepal', code : 'NP' },
    { name : 'Netherlands', code : 'NL' },
    { name : 'Netherlands Antilles', code : 'AN' },
    { name : 'New Caledonia', code : 'NC' },
    { name : 'New Zealand', code : 'NZ' },
    { name : 'Nicaragua', code : 'NI' },
    { name : 'Niger', code : 'NE' },
    { name : 'Nigeria', code : 'NG' },
    { name : 'Niue', code : 'NU' },
    { name : 'Norfolk Island', code : 'NF' },
    { name : 'Northern Mariana Islands', code : 'MP' },
    { name : 'Norway', code : 'NO' },
    { name : 'Oman', code : 'OM' },
    { name : 'Pakistan', code : 'PK' },
    { name : 'Palau', code : 'PW' },
    { name : 'Palestinian Territory, Occupied', code : 'PS' },
    { name : 'Panama', code : 'PA' },
    { name : 'Papua New Guinea', code : 'PG' },
    { name : 'Paraguay', code : 'PY' },
    { name : 'Peru', code : 'PE' },
    { name : 'Philippines', code : 'PH' },
    { name : 'Pitcairn', code : 'PN' },
    { name : 'Poland', code : 'PL' },
    { name : 'Portugal', code : 'PT' },
    { name : 'Puerto Rico', code : 'PR' },
    { name : 'Qatar', code : 'QA' },
    { name : 'Reunion', code : 'RE' },
    { name : 'Romania', code : 'RO' },
    { name : 'Russian Federation', code : 'RU' },
    { name : 'RWANDA', code : 'RW' },
    { name : 'Saint Helena', code : 'SH' },
    { name : 'Saint Kitts and Nevis', code : 'KN' },
    { name : 'Saint Lucia', code : 'LC' },
    { name : 'Saint Pierre and Miquelon', code : 'PM' },
    { name : 'Saint Vincent and the Grenadines', code : 'VC' },
    { name : 'Samoa', code : 'WS' },
    { name : 'San Marino', code : 'SM' },
    { name : 'Sao Tome and Principe', code : 'ST' },
    { name : 'Saudi Arabia', code : 'SA' },
    { name : 'Senegal', code : 'SN' },
    { name : 'Serbia and Montenegro', code : 'CS' },
    { name : 'Seychelles', code : 'SC' },
    { name : 'Sierra Leone', code : 'SL' },
    { name : 'Singapore', code : 'SG' },
    { name : 'Slovakia', code : 'SK' },
    { name : 'Slovenia', code : 'SI' },
    { name : 'Solomon Islands', code : 'SB' },
    { name : 'Somalia', code : 'SO' },
    { name : 'South Africa', code : 'ZA' },
    { name : 'South Georgia and the South Sandwich Islands', code : 'GS' },
    { name : 'Spain', code : 'ES' },
    { name : 'Sri Lanka', code : 'LK' },
    { name : 'Sudan', code : 'SD' },
    { name : 'Suriname', code : 'SR' },
    { name : 'Svalbard and Jan Mayen', code : 'SJ' },
    { name : 'Swaziland', code : 'SZ' },
    { name : 'Sweden', code : 'SE' },
    { name : 'Switzerland', code : 'CH' },
    { name : 'Syrian Arab Republic', code : 'SY' },
    { name : 'Taiwan, Province of China', code : 'TW' },
    { name : 'Tajikistan', code : 'TJ' },
    { name : 'Tanzania, United Republic of', code : 'TZ' },
    { name : 'Thailand', code : 'TH' },
    { name : 'Timor-Leste', code : 'TL' },
    { name : 'Togo', code : 'TG' },
    { name : 'Tokelau', code : 'TK' },
    { name : 'Tonga', code : 'TO' },
    { name : 'Trinidad and Tobago', code : 'TT' },
    { name : 'Tunisia', code : 'TN' },
    { name : 'Turkey', code : 'TR' },
    { name : 'Turkmenistan', code : 'TM' },
    { name : 'Turks and Caicos Islands', code : 'TC' },
    { name : 'Tuvalu', code : 'TV' },
    { name : 'Uganda', code : 'UG' },
    { name : 'Ukraine', code : 'UA' },
    { name : 'United Arab Emirates', code : 'AE' },
    { name : 'United Kingdom', code : 'GB' },
    { name : 'United States', code : 'US' },
    { name : 'United States Minor Outlying Islands', code : 'UM' },
    { name : 'Uruguay', code : 'UY' },
    { name : 'Uzbekistan', code : 'UZ' },
    { name : 'Vanuatu', code : 'VU' },
    { name : 'Venezuela', code : 'VE' },
    { name : 'Viet Nam', code : 'VN' },
    { name : 'Virgin Islands, British', code : 'VG' },
    { name : 'Virgin Islands, U.S.', code : 'VI' },
    { name : 'Wallis and Futuna', code : 'WF' },
    { name : 'Western Sahara', code : 'EH' },
    { name : 'Yemen', code : 'YE' },
    { name : 'Zambia', code : 'ZM' },
    { name : 'Zimbabwe', code : 'ZW' }
];

// Mock server endpoint for demo purposes
AjaxHelper.mockUrl('remotecombo', (url, params) => {
    const matches = params ?
        countries.filter(country => StringHelper.stripDiacritics(country.name.toLowerCase()).startsWith(StringHelper.stripDiacritics(params.filter.toLowerCase()))) :
        countries;
    return {
        responseText : JSON.stringify({
            success : true,
            total   : matches.length,
            data    : matches
        })
    };
});

Snippet: Loading data from simple string array

const combo = new Combo({
    items       : ['Small', 'Smaller', 'Really small', 'Tiny'],
    placeholder : 'Pick size of diamond for ring'
});

Loading data from array with item configs:

const combo = new Combo({
    items : [{ value: 'a', text: 'First' }, { value: 'z', text: 'Last' }]
});

Loading data from store:

const combo = new Combo({
    store        : memberStore,
    valueField   : 'id',
    displayField : 'name'
});

Editability

When configured with editable : false, the field may still be operated and have a value selected from the picker.

This setting just means that the textual input field may not be edited by the UI. In this mode, the picker's displayed dataset my still be filtered by typing while the picker is visible.

The combo's configured primaryFilter is used which uses the startsWith operator by default.

Grouping the list

To group the list contents, simply group your store using groupers. You can decide if clicking a header should toggle all group children (or if it should do nothing) with the allowGroupSelect flag.

const combo = new Combo({
    width            : 400,
    displayField     : 'name',
    valueField       : 'id',
    multiSelect      : true,
    picker : {
        allowGroupSelect : false,
        // Show icon based on group name
        groupHeaderTpl   : (record, groupName) => `
            <i class="icon-${groupName}"></i>${groupName}
        `
    },
    value : [1, 4],
    store : new Store({
        fields : [
            'type'
        ],
        groupers : [
            { field : 'type', ascending : true }
        ],
        data : [
            { id : 1, name : 'pizza', type : 'food' },
            { id : 2, name : 'bacon', type : 'food' },
            { id : 3, name : 'egg', type : 'food' },
            { id : 4, name : 'Gin tonic', type : 'drinks' },
            { id : 5, name : 'Wine', type : 'drinks' },
            { id : 6, name : 'Beer', type : 'drinks' }
        ]
    })
});

This demo uses multi-selection and a grouped list:

Multiselect combo
//<code-header>
fiddle.title = 'Multiselect combo';
CSSHelper.insertRule('.foodmenu-combo .b-chip i { margin-inline-end:0.4em }', targetElement.getRootNode());
CSSHelper.insertRule('.b-list-item-group-header { font-size:1.1em; }', targetElement.getRootNode());
CSSHelper.insertRule('.b-foodmenu-item { display:flex; flex-direction:column; align-items:flex-start; margin-inline-start:0.3em}', targetElement.getRootNode());
CSSHelper.insertRule('.b-foodmenu-item span.name { font-size:1.1em; }', targetElement.getRootNode());
CSSHelper.insertRule('.b-foodmenu-item small { display:block; color:#999; margin-top:0.3em;}', targetElement.getRootNode());
CSSHelper.insertRule('.b-foodmenu-item small { display:block; color:#999; margin-top:0.3em;}', targetElement.getRootNode());
//</code-header>
// multiSelect
new Combo({
    appendTo    : targetElement,
    multiSelect : true,
    label       : 'Skills - multiSelect',
    value       : ['Javascript', 'SQL'],
    items       : [
        'BASIC',
        'COBOL',
        'FORTRAN',
        'DBL',
        'SQL',
        'C/C++',
        'Java',
        'Javascript'
    ],
    width : '100%'
});

new Combo({
    appendTo     : targetElement,
    width        : '100%',
    multiSelect  : true,
    cls          : 'foodmenu-combo',
    style        : 'margin-top:3em',
    label        : 'Grouped list + multiSelect. Type to filter the picker when open',
    displayField : 'name',
    valueField   : 'id',
    value        : [1, 5, 9],
    listCls      : 'grouped-combo',
    picker       : {
        groupHeaderTpl : (record, groupName) => `
           <i class="fa fa-${groupName === 'Food' ? 'pizza-slice' : groupName === 'Drinks' ? 'wine-glass' : 'utensils'}" style="margin-inline-end:0.8em;"></i>${groupName}
        `
    },
    chipView : {
        iconTpl : (record) => `<i class="fa fa-${record.icon}"></i>`
    },

    listItemTpl : record => `
        <div class="b-foodmenu-item">
            <span class="name">${record.name}</span>
            <small>${record.calories} calories</small>
        </div>
    `,
    editable : false,
    store    : {
        fields : [
            'type',
            'calories',
            'icon'
        ],
        groupers : [
            { field : 'type', ascending : false }
        ],
        data : [
            { id : 1, name : 'Cheese sticks', type : 'starters', calories : 312, icon : 'cheese' },
            { id : 2, name : 'Fried onion rings', type : 'starters', calories : 234, icon : 'ring' },
            { id : 3, name : 'Hummus', type : 'starters', calories : 532, icon : 'seedling' },
            { id : 4, name : 'Fried fish', type : 'food', calories : 243, icon : 'fish' },
            { id : 5, name : 'Pizza', type : 'food', calories : 687, icon : 'pizza-slice' },
            { id : 6, name : 'Hamburger', type : 'food', calories : 734, icon : 'hamburger' },
            { id : 7, name : 'Hot dog', type : 'food', calories : 435, icon : 'hotdog' },
            { id : 8, name : 'Salad', type : 'food', calories : 112, icon : 'salad' },
            { id : 9, name : 'Gin tonic', type : 'drinks', calories : 145, icon : 'glass-martini' },
            { id : 10, name : 'Wine', type : 'drinks', calories : 150, icon : 'glass-wine' },
            { id : 11, name : 'Soda', type : 'drinks', calories : 205, icon : 'glass-citrus' },
            { id : 12, name : 'Beer', type : 'drinks', calories : 109, icon : 'beer' }
        ]
    }
});

Shared Stores

More than one Combo may share a Store if they are required to draw their values from a shared data set.

The only limitation here is that the characteristics of the filter that is applied to the store by typing are inherited from the first combo. So for example, all would be caseSensitive or all case-insensitve, and all would use the same filterOperator.

In the example below, all three email address inputs use the same store of recipients.

Email multiselect
//<code-header>
fiddle.title = 'Email multiselect';
//</code-header>
const
    emailAddresses = new Store({
        data : [
            { id : 10, name : 'Mike McGregor',    email : '[email protected]' },
            { id : 11, name : 'Linda Ewans',      email : '[email protected]' },
            { id : 12, name : 'Don Scott',        email : '[email protected]' },
            { id : 13, name : 'Karen Smith',      email : '[email protected]' },
            { id : 14, name : 'Doug Johnson',     email : '[email protected]' },
            { id : 15, name : 'Jenny Adams',      email : '[email protected]' },
            { id : 16, name : 'Daniel Williams',  email : '[email protected]' },
            { id : 17, name : 'Melissa Brown',    email : '[email protected]' },
            { id : 18, name : 'John Jones',       email : '[email protected]' },
            { id : 19, name : 'Jane Miller',      email : '[email protected]' },
            { id : 20, name : 'Theo Davis',       email : '[email protected]' },
            { id : 21, name : 'Lisa More',        email : '[email protected]' },
            { id : 22, name : 'Adam Wilson',      email : '[email protected]' },
            { id : 23, name : 'Mary Taylor',      email : '[email protected]' },
            { id : 24, name : 'Barbara Anderson', email : '[email protected]' },
            { id : 25, name : 'James Thomas',     email : '[email protected]' },
            { id : 26, name : 'David Jackson',    email : '[email protected]' }
        ]
    }),
    comboConfig = {
        type         : 'combo',
        label        : 'To',
        multiSelect  : true,
        store        : emailAddresses,
        displayField : 'name',
        chipView     : {
            tooltip : {
                cls         : 'b-recipient-card',
                newInstance : true,
                forSelector : '.b-chip',
                getHtml     : 'up.emailChipTooltip'
            }
        }
    };

new Panel({
    appendTo      : targetElement,
    title         : 'Compose',
    height        : 500,
    labelPosition : 'align-before',
    items         : {
        // Three almost identical Combos
        to  : { ...comboConfig, required : true },
        cc  : { ...comboConfig, label : 'CC' },
        bcc : { ...comboConfig, label : 'BCC' },

        // A text field for the subject
        subject : {
            type    : 'textfield',
            label   : 'Subject',
            onInput : 'up.onSubjectInput'
        },

        // And the body textaera
        body : {
            type     : 'textarea',
            label    : 'Message',
            height   : 100,
            required : true
        }
    },

    bbar : {
        items : {
            send : {
                text    : 'Send',
                onClick : 'up.onSendClick'
            }
        }
    },

    onSendClick() {
        this.items.forEach(i => i.syncInvalid());
        if (!this.items.every(i => i.isValid)) {
            Toast.show('Please complete the mandatory fields');
        }
        else {
            MessageDialog.alert({
                title   : 'Email system',
                message : 'Sent'
            });
        }
    },

    onSubjectInput({ value }) {
        this.title = value || 'Subject';
    },

    emailChipTooltip({ tip, activeTarget }) {
        const person = tip.owner.getRecordFromElement(activeTarget);

        return {
            children : [{
                className : 'b-recipient-card-name',
                text      : person.name
            }, {
                className : 'b-recipient-card-email',
                text      : person.email
            }]
        };
    }
});

This may be operated using the keyboard. ArrowDown opens the picker, and then ArrowDown and ArrowUp navigate the picker's options. Enter selects an active option in the picker. Escape closes the picker.

Configs

145

Common

editablePickerField
listenersEvents

Other

Enable caching of the last retrieved result until the timeout is reached.

Configure this either with:

This prevents unnecessary request roundtrips to the backend when reopening the picker and the filter has not been changed. The cache expiry timer is restarted on each outgoing request.

The query to return all records when the filter value is empty will only happen when minChars config is 0. This might be desirable in some use cases for a consistent experience (e.g. when widening the result by deleting characters from the search term, the picker will not close), but might also return a very large result set, so use with care.

Falsy or unparseable values or negative numbers mean no caching.

caseSensitive: Boolean= false

Configure as true to force case matching when filtering the dropdown list based upon the typed value.

A config object to configure the ChipView to display the selected value set when multiSelect is true.

For example the itemTpl or iconTpl might be configured to display richer chips for selected items.

clearTextOnPickerHide: Boolean= true

true to clear value typed to a multiselect combo when picker is collapsed

clearTextOnSelection: Boolean= true

Specify false to not clear value typed to a multiselect combo when an item is selected.

Set to true to clear this field when user empties the input element

createOnUnmatched: function | String | Boolean

If configured as true, this means that when an unmatched string is typed into the combo's input field, and ENTER, or the multiValueSeparator is typed, a new record will be created using the typed string as the displayField.

If configured as a function, or the name of a function in the owning component hierarchy, the function will be called passing the string and combo field instance and should return the record to add (if any).

The new record will be appended to the store, and the value selected.

If the Store is an AjaxStore, the new record will be eiligible for syncing to the database through its createUrl.

If the AjaxStore is configured to autoCommit, the record will be synced immediately. If the server does not accept the new addition, the field is placed temporarily into an invalid state with a message that explains this.

For example:

    new Combo({
        label : 'Employee name',
        store : employees,
        createOnUnmatched(name, combo) {
            name = validateEmployeeName(name);

            if (name) {
                return new Employee({
                    name,
                    email : generateEmployeeEmail(name)
                });
            }
            else {
                combo.setError('Invalid new employee name');
            }
        }
    });
ParameterTypeDescription
nameString

Record name

comboCombo

Combo instance

Returns: Model -

New record

displayField: String= text

Field used for item text when populating from store

Template function that can be used to customize the displayed value

ParameterTypeDescription
recordModel

The record to provide a textual value for

comboCombo

A reference to this Combo

Returns: String | null
emptyText: String

Text to display in the drop down when there are no items in the underlying store.

Defaults to use the noResults localization string ("No results" in En locale).

A function which creates an array of values for the filterParamName to pass any filters to the server upon load.

The default behaviour is just to set the parameter value to the filter's value, but the filter can be fully encoded for example:

   {
       encodeFilterParams(filters) {
           const result = [];

           for (const { property, operator, value, caseSensitive } of filters) {
               result.push(JSON.stringify({
                   field : property,
                   operator,
                   value,
                   caseSensitive
               }));
          }
       return result;
   }
ParameterTypeDescription
filtersObject[]

filters

Returns: Object[] -

array of values

filterOnEnter: Boolean

If false, filtering will be triggered once you exceed minChars. To filter only when hitting Enter key, set this to true;

The name of an operator type as implemented in operator to use when filtering the dropdown list based upon the typed value.

This defaults to 'startsWith', but the '*' operator may be used to match all values which contain the typed value.

Not used when filterParamName is set to cause remote dropdown loading. The exact filtering operation is up to the server.

If the dropdown is to be populated with a filtered query to a remote server, specify the name of the parameter to pass the typed string here. By default, the string is simply sent as the value of the parameter. For special encoding, configure the combo with encodeFilterParams

filterSelected: Boolean= false

When multiSelect is true, you may configure filterSelected as true to hide items in the dropdown when they are added to the selection. It will appear as if the requested item has "moved" into the field's ChipView.

By default, the picker is hidden on selection in single select mode, and remains to allow more selections when multiSelect is true. Setting this to a Boolean value can override that default.

hideTrigger: Boolean= false

Configure as true to hide the expand trigger. This is automatically set to true if remote filtering is enabled by setting the filterParamName config.

inlinePicker: Boolean= false

Configure this as true to render the dropdown list as a permanently visible list in the document flow immediately below the input area instead of as a popup.

This also hides the expand trigger since it is not needed.

items: Object[] | String[] | Object

Rows to display in the dropdown (list items).

If an object, the property names provide the value for the Combo, and the property values provide the displayed test in the list and input area eg:

items : {
    small  : 'Small',
    medium : 'Medium',
    large  : 'Large'
}

If an array, each entry may be

  • an object containing properties which must include the valueField and displayField which populates the dropdown with text and provides the corresponding field value.
  • An array whose first value provides the value for the Combo and whose second value provides the displayed test in the list and input area.
  • An array of values where the valueField and displayField are the same.

eg:

items : [
    {value : 'small',  text : 'Small'},
    {value : 'medium', text : 'Medium'},
    {value : 'large',  text : 'Large'},
]

or

items : [
    ['small',  'Small'],
    ['medium', 'Medium'],
    ['large',  'Large'],
]

or

items : [ 'Small', 'Medium', 'Large' ]

The delay in milliseconds to wait after the last keystroke before filtering the list.

This is a minimum of 300ms for remote filtering to keep network requests manageable, and defaults to 10ms for locally filtered stores.

listCls: String

CSS class to add to picker

listItemTpl: function

Template string used to render the list items in the dropdown list

new Combo({
    listItemTpl : ({ text }) => `<div class="combo-color-box ${text}"></div>${text}`,
    editable    : false,
    items       : [
        'Black',
        'Green',
        'Orange',
        'Pink',
        'Purple',
        'Red',
        'Teal'
    ]
});
ParameterTypeDescription
recordModel

The record representing the item being rendered

Returns: String | null
minChars: Number

The minimum string length to trigger the filtering, only relevant when editable is true.

This defaults to 1 in the case of local filtering, but 4 if the filterParamName is set to cause remote dropdown loading.

Set to true to allow selection of multiple values from the dropdown list.

Each value is displayed as a "Chip" to the left of the input area. Chips may be selected using the LEFT and RIGHT arrow keys and deleted using the DELETE key to remove values from the field. There is also a clickable close icon in each chip.

Use toggleAllIfCtrlPressed to implement "select all" behaviour.

{
    type   : 'combo',
    store  : myStore,
    picker : {
        toggleAllIfCtrlPressed : true
    }
}

A key value which, when typed in a multiSelect Combo, selects the currently active item in the picker, and clears the input field ready for another match to be typed.

overlayAnchor: Boolean= false

This implies that the picker will display an anchor pointer, but also means that the picker will align closer to the input field so that the pointer pierces the pickerAlignElement

Configuration object for the picker on initialization. Returns the picker instance at runtime.

For example:

new Combo({
    ...
    // configure the combobox picker
    picker : {
        listeners : {
            // prevent selection of item with id == 2
            beforeItem : ({ record }) => record.id !== 2
        }
    }
})
pickerWidth: Number | String

Width of picker, defaults to this combo's pickerAlignElement width

Optionally a Filter config object which the combo should use for filtering using the typed value. This may use a filterBy property to test its value against any field in the passed record.

{
    type          : 'combo',
    store         : myStore,
    primaryFilter : {
        filterBy(record) {
            if (this.value == null) {
                return true;
            }
            const value = this.value.toLowerCase();

            // Match typed value with forename or surname
            return record.forename.toLowerCase().startsWith(value)
                || record.surname.toLowerCase().startsWith(value);
        }
    }
}

Store used to populate items. Also accepts a Store config object

triggerAction: all | last | null= all

How to query the store upon click of the expand trigger. Specify one of these values:

  • 'all' - Clear the filter and display the whole dataset in the dropdown.
  • 'last' - Filter the dataset using the last filter value.
  • null/any other - Use the value in the input field to filter the dataset.
validateFilter: Boolean= true

true to cause the field to be in an invalid state while the typed filter string does not match a record in the store.

value: String | Number | String[] | Number[]Also a property

The initial value of this Combo box. In single select mode (default) it's a simple string value, for multiSelect mode, it should be an array of record ids.

valueField: String | null

Field used for item value when populating from store. Setting this to null will yield the selected record as the Combo's value.

Leaving this value undefined means using the idField of the modelClass of the store as the value field.

columnWidget
nameField
renditionTextField
rtlRTL
spanWidget

Accessibility

ariaLabelWidget
keyMapKeyMap

Container

inlineField

CSS

clsWidget
colorWidget
htmlClsWidget
styleWidget
uiWidget

DOM

adoptWidget
appendToWidget
contentWidget
datasetWidget
htmlWidget
idWidget
tagWidget
titleWidget

Field

maxLengthTextField
minLengthTextField

Float & align

alignWidget
anchorWidget
centeredWidget
draggableWidget
floatingWidget
xWidget
yWidget

Input element

Label

hintField
labelLabelable
labelClsLabelable
labelPositionLabelable
labelsField
labelWidthLabelable

Layout

alignSelfWidget
dockWidget
flexWidget
heightWidget
hiddenWidget
marginWidget
maxHeightWidget
maxWidthWidget
minHeightWidget
minWidthWidget
textAlignWidget
weightWidget
widthWidget

Misc

badgeBadge
dataFieldWidget
disabledWidget
localeClassLocalizable
localizableLocalizable
maskedWidget
ownerWidget
refWidget
rippleWidget
tabWidget
tooltipWidget

Picker

autoExpandPickerField

Scrolling

Properties

96

Class hierarchy

isCombo: Boolean= truereadonly
Identifies an object as an instance of Combo class, or subclass thereof.
isCombo: Boolean= truereadonlystatic
Identifies an object as an instance of Combo class, or subclass thereof.
isBadgeBadge
isDelayableDelayable
isEventsEvents
isFieldField
isFormulaFieldFormulaField
isKeyMapKeyMap
isLabelableLabelable
isLocalizableLocalizable
isPickerFieldPickerField
isTextFieldTextField
isValidatableValidatable
isWidgetWidget

Other

The name of an operator type as implemented in operator to use when filtering the dropdown list based upon the typed value.

This defaults to 'startsWith', but the '*' operator may be used to match all values which contain the typed value.

Not used when filterParamName is set to cause remote dropdown loading. The exact filtering operation is up to the server.

innerElements: (HTMLElement|DomConfig)[]readonly

Returns array of the Combo's inner HTML elements. This getter can be overriden.

isEmpty: Booleanreadonly

Returns true if this field has no selected records.

multiSelect: Boolean= falseAlso a config

Set to true to allow selection of multiple values from the dropdown list.

Each value is displayed as a "Chip" to the left of the input area. Chips may be selected using the LEFT and RIGHT arrow keys and deleted using the DELETE key to remove values from the field. There is also a clickable close icon in each chip.

Use toggleAllIfCtrlPressed to implement "select all" behaviour.

{
    type   : 'combo',
    store  : myStore,
    picker : {
        toggleAllIfCtrlPressed : true
    }
}

Configuration object for the picker on initialization. Returns the picker instance at runtime.

For example:

new Combo({
    ...
    // configure the combobox picker
    picker : {
        listeners : {
            // prevent selection of item with id == 2
            beforeItem : ({ record }) => record.id !== 2
        }
    }
})
queryLast: Stringreadonlystatic

A constant value for the triggerAction config to indicate that clicking the trigger should filter the dataset using the last filter query string, not the input field value.

record: Modelreadonly

Get selected record.

records: Model[]readonly

Get the selected record(s).

Store used to populate items. Also accepts a Store config object

value: Object | Number | StringAlso a config

Get/sets combo value, selects corresponding item in the list Setting null clears the field.

If multiSelect is true, then multiple values may be passed as an array. If the values are records, these become the selected record set held by valueCollection, and the value yielded by this field is an array of all the valueFields from the records.

A Collection which holds the currently selected records from the store which dictates this field's value.

Usually, this will contain one record, the record selected.

When multiSelect is true, there may be several records selected.

$namestaticWidget
columnWidget
formulaFormulaField
inputField
isValidField
renditionTextField
rtlRTL
spanWidget
typestaticWidget

Accessibility

keyMapKeyMap

CSS

clsWidget

DOM

appendToWidget
contentWidget
datasetWidget
elementWidget
htmlWidget
idWidget
styleWidget

Float & align

xWidget
yWidget

Layout

alignSelfWidget
flexWidget
heightWidget
marginWidget
maxHeightWidget
maxWidthWidget
minHeightWidget
minWidthWidget
widthWidget

Lifecycle

configBase

Misc

badgeBadge
cellInfoWidget
disabledWidget
errorTipValidatable
labelLabelable
localeHelperLocalizable
localeManagerLocalizable
refWidget
tabWidget
tooltipWidget

Visibility

hiddenWidget
isVisibleWidget

Widget hierarchy

ownerWidget
parentWidget

Functions

67

Configuration

applyDefaultsstaticBase

Events

Float & align

alignToWidget
setXYWidget
showByWidget
toFrontWidget

Lifecycle

createstaticWidget
destroystaticBase
initClassstaticWidget

Misc

attachTooltipstaticWidget
fromElementstaticWidget
fromSelectorstaticWidget
getByIdstaticWidget
isOfTypeNamestaticBase
mixinstaticBase
optionalLstaticLocalizable

Other

clearField
clearErrorValidatable
composeWidget
createOnFrameDelayable
disableWidget
enableWidget
focusWidget
getErrorsValidatable
LstaticLocalizable
maskWidget
onEvents
recomposeWidget
relayAllEvents
selectField
setErrorValidatable
triggerEvents
unEvents
unmaskWidget

Picker

hidePickerPickerField
showPickerPickerField
togglePickerPickerField

Visibility

hideWidget
showWidget

Widget hierarchy

closestWidget
containsWidget
eachWidgetPickerField
ownsWidget
queryWidget
queryAllWidget
upWidget

Events

20

The default action was performed (an item in the list was selected)

// Adding a listener using the "on" method
combo.on('action', ({ source, value, record, records, userAction }) => {

});
ParameterTypeDescription
sourceCombo

The combo

value*

The value of the selected record

recordModel

Selected record

recordsModel[]

Selected records as an array if multiSelect is true

userActionBoolean

true if the value change is due to user interaction

User typed into the field. Please note that the value attached to this event is the raw input field value and not the combos value

// Adding a listener using the "on" method
combo.on('input', ({ source, value, event }) => {

});
ParameterTypeDescription
sourceCombo

The combo.

valueString

Raw input value

eventEvent

The triggering DOM event if any.

An item in the list was selected

// Adding a listener using the "on" method
combo.on('select', ({ source, record, records, userAction }) => {

});
ParameterTypeDescription
sourceCombo

The combo

recordModel

Selected record

recordsModel[]

Selected records as an array if multiSelect is true

userActionBoolean

true if the value change is due to user interaction

catchAllEvents
changeField
clearField
destroyEvents
focusInWidget
focusOutWidget
hideWidget
paintWidget
readOnlyWidget
recomposeWidget
resizeWidget
showWidget
triggerField

Event handlers

20

The default action was performed (an item in the list was selected)

new Combo({
    onAction({ source, value, record, records, userAction }) {

    }
});
ParameterTypeDescription
sourceCombo

The combo

value*

The value of the selected record

recordModel

Selected record

recordsModel[]

Selected records as an array if multiSelect is true

userActionBoolean

true if the value change is due to user interaction

User typed into the field. Please note that the value attached to this event is the raw input field value and not the combos value

new Combo({
    onInput({ source, value, event }) {

    }
});
ParameterTypeDescription
sourceCombo

The combo.

valueString

Raw input value

eventEvent

The triggering DOM event if any.

An item in the list was selected

new Combo({
    onSelect({ source, record, records, userAction }) {

    }
});
ParameterTypeDescription
sourceCombo

The combo

recordModel

Selected record

recordsModel[]

Selected records as an array if multiSelect is true

userActionBoolean

true if the value change is due to user interaction

onClearField
onDestroyEvents
onFocusInWidget
onHideWidget
onPaintWidget
onResizeWidget
onShowWidget

Typedefs

7

CSS variables

132
NameDescription
--b-combo-filled-chip-view-padding-topFilledTop padding for chipview in combo when using "filled" rendition
--b-combo-filled-label-before-chip-view-padding-topFilledTop padding for chipview in combo with label displayed before and using "filled" rendition
--b-combo-outlined-chip-view-padding-topOutlinedTop padding for chipview in combo when using "outlined" rendition
--b-combo-outlined-label-before-chip-view-padding-topOutlinedTop padding for chipview in combo with label displayed before and using "outlined" rendition
--b-combo-chip-font-sizeFont size for chipview in combo
--b-combo-chip-view-margin-blockBlock margin for chipview in combo
--b-combo-chip-view-paddingPadding for chipview in combo
--b-combo-chip-view-min-heightMinimum height for chipview in combo (to avoid it collapsing when empty)