6

I am trying to group results that I am getting from store to be displayed inside ComboBox . I have a combobox that looks like this: enter image description here

and I need it to look like this :

enter image description here

That means grouped by category (order / invoice ).

My combobox defined like this :

Ext.define('NG.view.searchcombo.Combo', {
    requires: ['Ext.form.field.ComboBox'],
    extend: 'Ext.form.ComboBox',
    alias: 'widget.searchcombo',
    minChars:3,
    fieldLabel: 'Choose Search',
    store: 'Search',
    displayField: 'name',
    valueField: 'id',
    typeAhead: false,
    hideLabel: true,
    hideTrigger:false,
    anchor: '100%',

    listConfig: {
        loadingText: 'Searching...',
        emptyText: 'No matching posts found.',

        // Custom rendering template for each item
        getInnerTpl: function() {
            return '<h3>{name} / {category}</h3>' +'{excerpt}' ;
        }
    },
    pageSize: 10,
    initComponent: function () {    

        this.callParent(arguments);
    }
});

and my data is like this :

[{
    "id": 1,
    "name": "one",
    "category": "invoice"
}, {
    "id": 2,
    "name": "two",
    "category": "invoice"
}, {
    "id": 3,
    "name": "one",
    "category": "order"
}, {
    "id": 4,
    "name": "two",
    "category": "order"
}, {
    "id": 5,
    "name": "three",
    "category": "invoice"
}, {
    "id": 6,
    "name": "four",
    "category": "invoice"
}, {
    "id": 7,
    "name": "three",
    "category": "order"
}, {
    "id": 8,
    "name": "four",
    "category": "order"
}, {
    "id": 9,
    "name": "five",
    "category": "invoice"
}, {
    "id": 10,
    "name": "six",
    "category": "invoice"
}, {
    "id": 11,
    "name": "five",
    "category": "order"
}, {
    "id": 12,
    "name": "six",
    "category": "order"
}, {
    "id": 13,
    "name": "seven",
    "category": "invoice"
}, {
    "id": 14,
    "name": "eight",
    "category": "invoice"
}, {
    "id": 15,
    "name": "seven",
    "category": "order"
}, {
    "id": 16,
    "name": "eight",
    "category": "order"
}]

I think that can be done by using Ext.XTemplate but I am not familiar with Ext.XTemplate.

Narendra Jadhav
  • 8,799
  • 15
  • 28
  • 38
Polina F.
  • 595
  • 10
  • 28

5 Answers5

13

I wanted a much simpler solution, so I'll share what I came up with.

For my purposes, I had a key that I wanted to group on, which is a single character. I knew the headers I wanted to show for each key, so I pre-sorted the list to make sure the types came together, and then I just render a group header each time I see a new key.

myStore.sort('key', 'DESC');

Ext.create('Ext.form.field.ComboBox', {
  store: myStore,
  queryMode: 'local',
  displayField: 'name',
  valueField: 'id',
  listConfig: {
    cls: 'grouped-list'
  },
  tpl: Ext.create('Ext.XTemplate',
    '{[this.currentKey = null]}' +
    '<tpl for=".">',
      '<tpl if="this.shouldShowHeader(key)">' +
        '<div class="group-header">{[this.showHeader(values.key)]}</div>' +
      '</tpl>' +
      '<div class="x-boundlist-item">{name}</div>',
    '</tpl>',
    {
      shouldShowHeader: function(key){
        return this.currentKey != key;
      },
      showHeader: function(key){
        this.currentKey = key;
        switch (key) {
          case 's': return 'Structures';
          case 'f': return 'Filters';
          ...
        }
        return 'Other';
      }
    }
  )
});

Using the following CSS:

.grouped-list .x-boundlist-item {
  padding: 1px 3px 0 10px
}

.grouped-list .group-header {
  padding: 4px;
  font-weight: bold;
  border-bottom: 1px solid #ddd;
}

And this data:

[
    { key: 's', name: '2014 Product Development' },
    { key: 'f', name: 'Message Filter' },
    { key: 's', name: '2014 Product Development (Little)' },
    { key: 's', name: 'Global Structure' },
    { key: 'f', name: 'My SW' }
]

I get a nice looking grouped list like this:

Sean Adkinson
  • 7,869
  • 3
  • 39
  • 62
  • Can you please sample data which you are binding to the combobox. Thanks !! – SharpCoder Jun 09 '14 at 13:13
  • the problem with this solution is that the code has knowledge about the content of the data. this limits it's usage to a specific data set. Perhaps the data should carry information about the headers, e.g. if a record is in fact a header. – Tsahi Asher Oct 27 '14 at 12:57
  • @TsahiAsher That is true. This was the solution for my scenario. It could easily be adapted to a situation where the data included `group` instead of `key`, and then the template would just be updated. – Sean Adkinson Oct 27 '14 at 16:25
  • Thanks ! worked as a charm. But, I am getting "null" as a first header which is additional and not required ! any idea how to avoid it ? I think it is because of `'{[this.currentKey = null]}' +` – Saurabh Bayani Mar 10 '15 at 14:01
  • Hmm, that makes sense that it would output "null", but it must've just been hidden for me. I think you can just remove that initialization line altogether, since whether it is initially null or undefined shouldn't matter. – Sean Adkinson Mar 11 '15 at 16:09
  • @SaurabhBayani you can replace it with `{%this.currentKey = null%}`, so it's added to the template code but not written to the page. – Tsahi Asher Sep 20 '15 at 06:32
7

This is an extension that improves on Sean Adkinson answer above by making a reusable component from his code.

I have had mixed results with replacing BoundList with a GridPanel with Ext 5.0.1 there for this is what I used.

One caveat it does not support collapsing the groups but it works great for me.

Tested in Extjs 4.2.3 and 5.0.1.

You can see it in Sencha fiddle

Hope it helps someone out there.

Ext.define('Ext.ux.GroupComboBox', {
  extend: 'Ext.form.field.ComboBox',
  alias: 'widget.groupcombobox',
  /*
   * @cfg groupField String value of field to groupBy, set this to any field in your model
   */
  groupField: 'group',
  listConfig: {
    cls: 'grouped-list'
  },
  initComponent: function() {
    var me = this;
    me.tpl = new Ext.XTemplate([
      '{%this.currentGroup = null%}',
      '<tpl for=".">',
      '   <tpl if="this.shouldShowHeader(' + me.groupField + ')">',
      '       <div class="group-header">{[this.showHeader(values.' + me.groupField + ')]}</div>',
      '   </tpl>',
      '   <div class="x-boundlist-item">{' + me.displayField + '}</div>',
      '</tpl>', {
        shouldShowHeader: function(group) {
          return this.currentGroup != group;
        },
        showHeader: function(group) {
          this.currentGroup = group;
          return group;
        }
      }
    ]);
    me.callParent(arguments);
  }
});

//Example usage
var Restaurants = Ext.create('Ext.data.Store', {
  storeId: 'restaraunts',
  fields: ['name', 'cuisine'],
  sorters: ['cuisine', 'name'],
  groupField: 'cuisine',
  data: [{
    name: 'Cheesecake Factory',
    cuisine: 'American'
  }, {
    name: 'University Cafe',
    cuisine: 'American'
  }, {
    name: 'Creamery',
    cuisine: 'American'
  }, {
    name: 'Old Pro',
    cuisine: 'American'
  }, {
    name: 'Nola\'s',
    cuisine: 'Cajun'
  }, {
    name: 'House of Bagels',
    cuisine: 'Bagels'
  }, {
    name: 'The Prolific Oven',
    cuisine: 'Sandwiches'
  }, {
    name: 'La Strada',
    cuisine: 'Italian'
  }, {
    name: 'Buca di Beppo',
    cuisine: 'Italian'
  }, {
    name: 'Pasta?',
    cuisine: 'Italian'
  }, {
    name: 'Madame Tam',
    cuisine: 'Asian'
  }, {
    name: 'Sprout Cafe',
    cuisine: 'Salad'
  }, {
    name: 'Pluto\'s',
    cuisine: 'Salad'
  }, {
    name: 'Junoon',
    cuisine: 'Indian'
  }, {
    name: 'Bistro Maxine',
    cuisine: 'French'
  }, {
    name: 'Three Seasons',
    cuisine: 'Vietnamese'
  }, {
    name: 'Sancho\'s Taquira',
    cuisine: 'Mexican'
  }, {
    name: 'Reposado',
    cuisine: 'Mexican'
  }, {
    name: 'Siam Royal',
    cuisine: 'Thai'
  }, {
    name: 'Krung Siam',
    cuisine: 'Thai'
  }, {
    name: 'Thaiphoon',
    cuisine: 'Thai'
  }, {
    name: 'Tamarine',
    cuisine: 'Vietnamese'
  }, {
    name: 'Joya',
    cuisine: 'Tapas'
  }, {
    name: 'Jing Jing',
    cuisine: 'Chinese'
  }, {
    name: 'Patxi\'s Pizza',
    cuisine: 'Pizza'
  }, {
    name: 'Evvia Estiatorio',
    cuisine: 'Mediterranean'
  }, {
    name: 'Gyros-Gyros',
    cuisine: 'Mediterranean'
  }, {
    name: 'Mango Caribbean Cafe',
    cuisine: 'Caribbean'
  }, {
    name: 'Coconuts Caribbean Restaurant &amp; Bar',
    cuisine: 'Caribbean'
  }, {
    name: 'Rose &amp; Crown',
    cuisine: 'English'
  }, {
    name: 'Baklava',
    cuisine: 'Mediterranean'
  }, {
    name: 'Mandarin Gourmet',
    cuisine: 'Chinese'
  }, {
    name: 'Bangkok Cuisine',
    cuisine: 'Thai'
  }, {
    name: 'Darbar Indian Cuisine',
    cuisine: 'Indian'
  }, {
    name: 'Mantra',
    cuisine: 'Indian'
  }, {
    name: 'Janta',
    cuisine: 'Indian'
  }, {
    name: 'Starbucks',
    cuisine: 'Coffee'
  }, {
    name: 'Peet\'s Coffee',
    cuisine: 'Coffee'
  }, {
    name: 'Coupa Cafe',
    cuisine: 'Coffee'
  }, {
    name: 'Lytton Coffee Company',
    cuisine: 'Coffee'
  }, {
    name: 'Il Fornaio',
    cuisine: 'Italian'
  }, {
    name: 'Lavanda',
    cuisine: 'Mediterranean'
  }, {
    name: 'MacArthur Park',
    cuisine: 'American'
  }, {
    name: 'St Michael\'s Alley',
    cuisine: 'Californian'
  }, {
    name: 'Cafe Renzo',
    cuisine: 'Italian'
  }, {
    name: 'Miyake',
    cuisine: 'Sushi'
  }, {
    name: 'Sushi Tomo',
    cuisine: 'Sushi'
  }, {
    name: 'Kanpai',
    cuisine: 'Sushi'
  }, {
    name: 'Pizza My Heart',
    cuisine: 'Pizza'
  }, {
    name: 'New York Pizza',
    cuisine: 'Pizza'
  }, {
    name: 'Loving Hut',
    cuisine: 'Vegan'
  }, {
    name: 'Garden Fresh',
    cuisine: 'Vegan'
  }, {
    name: 'Cafe Epi',
    cuisine: 'French'
  }, {
    name: 'Tai Pan',
    cuisine: 'Chinese'
  }]
});

Ext.create('Ext.container.Viewport', {
  items: Ext.create('Ext.ux.GroupComboBox', {
    fieldLabel: 'Restaurants',
    name: 'txtRestaurant',
    forceSelection: true,
    editable: false,
    queryMode: 'local',
    triggerAction: 'all',
    multiSelect: true,
    groupField: 'cuisine',
    displayField: 'name',
    valueField: 'name',
    store: Restaurants,
    width: 400
  })
}).show();
.grouped-list .x-boundlist-item {
  padding: 1px 3px 0 10px;
}
.grouped-list .group-header {
  padding: 4px;
  font-weight: bold;
  border-bottom: 1px solid #ddd;
}
Tsahi Asher
  • 1,721
  • 17
  • 23
mistik1
  • 71
  • 1
  • 1
  • I like this one. I fixed a small bug in the template (waiting review), and I would like it better if the template used
      instead of
      .
    – Tsahi Asher Sep 17 '15 at 15:48
  • Nice extension. Can you help me with the following. When I use it in Ext 5.0.0, the groupFields are doubled each time I type to filter. See this fork on your fiddle: https://fiddle.sencha.com/#fiddle/ugi – Christiaan Westerbeek Sep 25 '15 at 12:48
  • This is nice, thanks for cleaning up my solution to be more reusable :+1: – Sean Adkinson Mar 10 '17 at 18:27
1

Please, you can get this done using a Grid to Render your combobox content. Refer to this post: http://www.sencha.com/forum/showthread.php?132328-CLOSED-ComboBox-using-Grid-instead-of-BoundList

Following the article I was able to create this:

enter image description here

Guilherme Lopes
  • 4,617
  • 1
  • 15
  • 34
0

That is the code the worked for me:

If you are using Sencha Architect, add the createPicker inside an Override and manually create the listConfig as an Object.

{
    xtype: 'combobox',
    createPicker: function() {
        var me = this,
            picker,
            menuCls = Ext.baseCSSPrefix + 'menu',
            opts = Ext.apply({
                selModel: {
                    mode: me.multiSelect ? 'SIMPLE' : 'SINGLE'
                },
                floating: true,
                hidden: true,
                ownerCt: me.ownerCt,
                cls: me.el.up('.' + menuCls) ? menuCls : '',
                store: me.store,
                displayField: me.displayField,
                focusOnToFront: false,
                pageSize: me.pageSize
            }, me.listConfig, me.defaultListConfig);

        // NOTE: we simply use a grid panel
        //picker = me.picker = Ext.create('Ext.view.BoundList', opts);


        picker = me.picker = Ext.create('Ext.grid.Panel', opts);

        // hack: pass getNode() to the view
        picker.getNode = function() {
            picker.getView().getNode(arguments);
        };

        me.mon(picker.getView(), {
            refresh: me.onListRefresh,
            scope: me
        });
        me.mon(picker, {
            itemclick: me.onItemClick,
            //            refresh: me.onListRefresh,
            scope: me
        });

        me.mon(picker.getSelectionModel(), {
            selectionChange: me.onListSelectionChange,
            scope: me
        });

        return picker;
    },
    listConfig: {
        columns: [{
            xtype: "gridcolumn",
            dataIndex: "id",
            text: "Id"
        }, {
            xtype: "gridcolumn",
            dataIndex: "name",
            text: "Name"
        }],
        features: [{
            ftype: "grouping"
        }]
    },
    fieldLabel: 'Label',
    queryMode: 'local',
    store: 'myTestStore'
}
Narendra Jadhav
  • 8,799
  • 15
  • 28
  • 38
Guilherme Lopes
  • 4,617
  • 1
  • 15
  • 34
0

I've implemented my own version of the combo with a grid as its list component. You can get it on GitHub, and I've put some examples online.

The 3rd example closely matches what you're trying to achieve.

Here's an example that would match even more closely. You'd only be left with the styling to do:

Ext.widget('gridpicker', {

    queryMode: 'local'
    ,displayField: 'name'

    ,store: {
        fields: ['name', 'group']
        ,proxy: {type: 'memory', reader: 'array'}
        ,data: ...
        ,groupField: 'group'
        ,sorters: {property: 'name', order: 'ASC'}
    }

    ,gridConfig: {
        features: [{
            ftype:'grouping'
            ,groupHeaderTpl: '{name}'
            ,collapsible: false
        }]
        ,columns: [{
            width: 30
            ,renderer: function(value, md, record, rowIndex) {
                return '<img src="..." />';
            }
        },{
            dataIndex: 'name'
            ,flex: 1
        }]
    }
});
rixo
  • 18,198
  • 3
  • 35
  • 50