Summary: Learn how to control the order of groups in a sorted list. You don’t do it directly with the grouper function, you do it with the sorter function.

One of the features of the app that the participants build in the CD168 sessions at SAP TechEd Amsterdam is a list of sales orders that can be grouped according to status or price (the screenshot shows the orders grouped by price).

groupbyprice.PNGThis is achieved by specifying a value for the vGroup parameter on the Sorter, as documented in the sap.ui.model.Sorter API reference:

Configure grouping of the content, can either be true to enable grouping based on the raw model property value, or a function which calculates the group value out of the context (e.g. oContext.getProperty(“date”).getYear() for year grouping). The control needs to implement the grouping behaviour for the aggregation which you want to group.

So what this means is that you either specify a boolean true value, or the name of a function.

– use a boolean true to have the entries grouped “naturally” by their value: useful and useable where you have texts that will be the same for some entries

– specify the name of a function that will do some custom grouping: useful where you have numeric values that you might want to group into sizes or ranges

Here in the example in the screenshot on the left, we’re using a custom grouper function to arrange the sales orders into value groups (less than EUR 5000, less than EUR 10,000 and more than EUR 10,000).

Group Order Challenge

But what if you wanted to influence not only the sort but also the order of the groups themselves? Specifically in this screenshot example, what if we wanted to have the “< 5000 EUR” group appear first, then the “> 10,000 EUR” group and finally the “< 10,000 EUR” group? (This is a somewhat contrived example but you get the idea). This very question is one I was asking myself while preparing for the CD168 session, and also one I was asked by an attendee.

To understand how to do it, you have to understand that the relationship between the sorter and the grouper can be seen as a “master / slave” relationship. This is in fact reflected in how you specify the grouper – as a subordinate of the master.

The sorter drives everything, and the grouper just gets a chance to come along for the ride.

So to answer the question, and to illustrate it in code step by step, I’ve put together an example. It takes a simple list of numbers 1 to 30 and displays them in a list, and groups them into three size categories. You can specify in which order the groups appear, but the key mechanism to achieve this, as you’ll see, is actually in the sorter.

Simple and Complex Sorting

To understand further, you have to remember that there’s a simple sorter specification and a more complex one. Using a simple sorter is often the case, and you’d specify it like this:


new sap.m.List("list", {
    items: {
        path: '/records',
        template: new sap.m.StandardListItem({
            title: '{amount}'
        }),
        sorter: new sap.ui.model.Sorter("amount") // <---
    }
})

This is nice and simple and sorts based on the value of the amount property, default ascending.

The complex sorter is where you can specify your own custom sorting logic, and you do that by creating an instance of a Sorter and then specifying your custom logic for the fnCompare function.

We’ll be using the sorter with its own custom sorting logic.

Step By Step

So here’s the example, described step by step. It’s also available as a Gist on Github: Custom Sorter and Grouper in SAPUI5 and exposed in a runtime context using the bl.ocks.org facility: http://bl.ocks.org/qmacro/7702371

As the source code is available in the Gist, I won’t bother showing you the HTML and SAPUI5 bootstrap, I’ll just explain the main code base.


        var sSM = 10;  // < 10  Small
        var sML = 15;  // < 15  Medium
                                  //    15+ Large

Here we just specify the boundary values for chunking our items up into groups. Anything less than 10 is “Small”, less than 15 is “Medium”, otherwise it’s “Large”. I’ve deliberately chosen groupings that are not of equal size (the range is 1-30) just for a better visual example effect.


        // Generate the list of numbers and assign to a model
        var aValues = [];
        for (var i = 0; i < 30; i++) aValues.push(i);
        sap.ui.getCore().setModel(
            new sap.ui.model.json.JSONModel({
                records: aValues.map(function(v) { return { value: v }; })
            })
        );

So we generate a list of numbers (I was really missing Python’s xrange here, apropo of nothing!) and add it as a model to the core.


        // Sort order and title texts of the S/M/L groups
        var mGroupInfo = {
            S: { order: 2, text: "Small"},
            M: { order: 1, text: "Medium"},
            L: { order: 3, text: "Large"}
        }

Here I’ve created a map object that specifies the order in which the Small, Medium and Large groups should appear in the list (Medium first, then Small, then Large). The texts are what should be displayed in the group subheader/dividers in the list display.


        // Returns to what group (S/M/L) a value belongs
        var fGroup = function(v) {
            return v < sSM ? "S" : v < sML ? "M" : "L";
        }

This is just a helper function to return which size category (S, M or L) a given value belongs to.


        // Grouper function to be supplied as 3rd parm to Sorter
        // Note that it uses the mGroupInfo, as does the Sorter
        var fGrouper = function(oContext) {
            var v = oContext.getProperty("value");
            var group = fGroup(v);
            return { key: group, text: mGroupInfo[group].text };
        }

Here’s our custom Grouper function that will be supplied as the third parameter to the Sorter. It pulls the value of the property from the context object it receives, uses the fGroup function (above) to determine the size category, and then returns what a group function should return – an object with key and text properties that are then used in the display of the bound items.


        // The Sorter, with a custom compare function, and the Grouper
        var oSorter = new sap.ui.model.Sorter("value", null, fGrouper);
        oSorter.fnCompare = function(a, b) {
            // Determine the group and group order
            var agroup = mGroupInfo[fGroup(a)].order;
            var bgroup = mGroupInfo[fGroup(b)].order;
            // Return sort result, by group ...
            if (agroup < bgroup) return -1;
            if (agroup > bgroup) return  1;
             // ... and then within group (when relevant)
            if (a < b) return -1;
            if (a == b) return 0;
            if (a > b) return  1;
        }

Here’s our custom Sorter. We create one as normal, specifying the fact that we want the “value” property to be the basis of our sorting. The ‘null’ is specified in the ascending/descending position (default is ascending), and then we specify our Grouper function. Remember, the grouper just hitches a ride on the sorter.

Because we want to influence the sort order of the groups as well as the order of the items within each group, we have to determine to what group each of the two values to be compared belong. If the groups are different, we just return the sort result (-1 or 1) at the group level. But if the two values are in the same group then we have to make sure that the sort result is returned for the items themselves.


        // Simple List in a Page
        new sap.m.App({
            pages: [
                new sap.m.Page({
                    title: "Sorted Groupings",
                    content: [
                        new sap.m.List("list", {
                            items: {
                                path: '/records',
                                template: new sap.m.StandardListItem({
                                    title: '{value}'
                                }),
                                sorter: oSorter
                            }
                        })
                    ]
                })
            ]
        }).placeAt("content");

And that’s pretty much it. Once we’ve done the hard work of writing our custom sorting logic, and shared the group determination between the Sorter and the Grouper (DRY!) we can just specify the custom Sorter in our binding of the items.

And presto! We have what we want – a sorted list of items, grouped, and those groups also in an order that we specify.

sortedgroupings.PNG

Post Script

There was a comment on this post which was very interesting and described a situation where you want to sort, and group, based on different properties. This is also possible. To achieve sorting on one property and grouping based on another, you have to recall that you can pass either a single Sorter object or an array of them, in the binding.

So let’s say you have an array of records in your data model, and these records have a “beerName” and a “beerType” property. You want to group by beerType, and within beerType you want the actual beerNames sorted.

In this case, you could have two Sorters: One for the beerType, with a Grouper function, and another for the beerName. Like this:


        var fGrouper = function(oContext) {
            var sType = oContext.getProperty("beerType") || "Undefined";
            return { key: sType, value: sType }
        }
        new sap.m.App({
            pages: [
                new sap.m.Page({
                    title: "Craft Beer",
                    content: [
                        new sap.m.List("list", {
                            items: {
                                path: '/',
                                template: new sap.m.StandardListItem({
                                    title: "{beerName}",
                                    description: "{beerType}"
                                }),
                                sorter: [
                                    new sap.ui.model.Sorter("beerType", null, fGrouper),
                                    new sap.ui.model.Sorter("beerName", null, null)
                                ]
                            }
                        })
                    ]
                })
            ]
        }).placeAt("content");

I’ve put a complete example together for this, and it’s in the sapui5bin Github repo here:

sapui5bin/SortingAndGrouping/TwoProperties.html at master · qmacro/sapui5bin · GitHub

And while we’re on the subject of code examples, there’s a complete example for the main theme of this post here:

sapui5bin/SortingAndGrouping/SingleProperty.html at master · qmacro/sapui5bin · GitHub

    

Share & enjoy!

To report this post you need to login first.

18 Comments

You must be Logged on to comment or reply to a post.

  1. Robin van het Hof

    This is stunning! I have been struggling to get something similar to work, but couldn’t get the complex sorter mechanism quite right…

    Your example makes perfect sense to me, so thanks for helping me out in advance! I foresee a well-spent coding weekend ahead… 😉

    (1) 
  2. Raymond Does

    Actually this only seems to work in situations where you apply grouping and sorting on the same property. In this case the ‘value’ property. I needed to group a specific property (‘type’) and sort on another one (‘companyName’) and came up with the following solution: first, do the sorting in the controller’s onInit function. Then, in the view, create a sorter with a grouping function. In the grouping function, group on a property. In my case on a sponsor type. Then add a compare function to the sorter but leave it empty.

    Controller onInit:

    1. sponsorModelData.sort(function(a, b) {

           var aTYPE = (a[“type”] == “Gold”) ? 1 : (a[“type”] == “Silver”) ? 2 : 3;

           var bTYPE = (b[“type”] == “Gold”) ? 1 : (b[“type”] == “Silver”) ? 2 : 3;

           var aCOMPNAME = a[“companyName”];

           var bCOMPNAME = b[“companyName”];                      

                               

           if(aTYPE == bTYPE){

                   return (aCOMPNAME < bCOMPNAME) ? -1 : (aCOMPNAME > bCOMPNAME) ? 1 : 0;

        }

           else

        {

                   return (aTYPE < bTYPE) ? -1 : 1;

        }

                               

    });

    And the View :

    var grouper = function(oContext) { 

                var v = oContext.getObject();   

                return { key: v.type, text: v.type }; 

    }; 

                 

    var oSorter = new sap.ui.model.Sorter(“companyName”, null, grouper); 

    oSorter.fnCompare = function(a, b) {

    // Do not sort here, sort is done in controller’s onInit             

    }; 

    oSponsorList.bindAggregation(“items”“/”, sponsorListTemplate, oSorter);

    Now the sponsors are sorted by type: Gold, Silver and Bronze and within each group sorted on companyName.

    (0) 
    1. DJ Adams Post author

      Hi there

      Thanks for the interesting comment!

      I’ve updated this post to take this sort of circumstance also into account. Rather than pre-sort the data and have an empty custom sort function I’m using an array of two Sorters, one with a Grouper connection.

      I’ve also posted a full example for this in my sapui5bin Github repo.

      cheers

      dj

      (0) 
    1. Krishna Kumar S

      Hi,

      Thanks for the neat blog on grouping. i want to know if the grouping is supported for sap.ui.table.Table control?

      what i need is to group the rows in the table in a specific order and then sort within the group. is this possible?

      Many thanks,

      Regards,Krishna

      (0) 
  3. Holger Schäfer

    Hi DJ,

    interessting blog on advanced sorting and grouping.

    Currently i have some issues using a xml model and floats that will always be sorted as text. Inside the bindings sourcecode there is a switch to use datatype specific sorting.

    I added the float type directly to my binding with no result.

    I am using a xmlview with

    data-sap-ui-xx-bindingSyntax=”complex


    the used mode seems to be experimentall and i think maybe binding types is currently not supported.


    If i have a xmlmodel, is it possible to set datatypes directly on the model?

    The XML Data seems to be typeless instead of using JSON Model (all contents are transfered as strings)


    When binding the type inside the view i have to bind the same type for each used binding.

    What is the best way to teach a xmlmodel the datatypes for the properties?

    Cheers Holger

    (0) 
  4. Rashmi Nagendra

    Hi DJ,

    I am trying to sort & filter the table as given the example SAPUI5 Explored. I am having SAP backend with OData as model. Neither sort nor filter works.  On sorting I get an error. Do we need any backend coding for sorting & filtering?

    Thanks,

    Rashmi

    (0) 
    1. DJ Adams Post author

      “Get an error” – can you be a little more specific?

      But yes, the ODataModel is a server side mechanism, sorting and filtering etc is done on the server, i.e. backend.

      (0) 
  5. Siyu Henningsen

    Hi DJ,

    Thanks for the great blog. I have a question and hope you have a suggestion for it.

    How do I specify a custom compare function for Sorter in XML View drop down menu?

    All the blogs I found use compare function are written in JS View.

    I love XML view and hope to find a way to do this in XML View.

    The sap.ui.model.Sorter API reference only specifies the sPath, bDescending and vGroup.

    I tried to add fnCompare as a property to Sorter’s declaration in the XML view, that doesn’t work. For example:

    <Select id=”categorySelect” items=”{ path:’/categories’, sorter:{ path:’name’, descending: ‘true’, fnCompare: ‘compareNameByTranslation’}}” change=”onCategorySelected”>

         <items>

                <core:Item text=”{ path:’name’, formatter:’util.getI18nName’}” key=”{id}”>

                     <core:customData>

                          <core:CustomData key=”id” value=”{id}” />

                     </core:customData>

                </core:Item>

          </items>

    </Select>

    When I don’t specify the fnCompare, the list is sorted by name in English.

    But since my name data is an object which has translation in it like below.

      “name”:{

                “de”:”laptop(de)”,

                “en”:”laptop”

             }

    I need the custom compare function to achieve the following:

    1. Retrieve the correct translation first, then compare them. i.e. I need to retrieve name.en = “laptop”.

    2. Need to have the capability to alter the order, so that I can put a specific name as a default value for the drop down before the view is rendered.

    Any suggestion will be greatly appreciated!

    Best Regards,

    Siyu

    (0) 
  6. Siyu Henningsen

    Okay, after searched around, I didn’t find any way to add fnCompare to XML view Sorter declaration directly. I did find a work around:

    1. In onBeforeShow method, attach the fnCompare to the Sorter .

    2. Then set the default value through setSearchKey.

    That it!

    (0) 
  7. Naveen Kudari

    hi DJ,

    i had a requirement of sorting and grouping some what related to your code, i understood everything except this logic,

              oSorter.fnCompare = function(a, b) { 

                // Determine the group and group order 

                var agroup = mGroupInfo[fGroup(a)].order; 

                var bgroup = mGroupInfo[fGroup(b)].order; 

               console.log(agroup);

                console.log(bgroup);

              // Return sort result, by group … 

                if (agroup < bgroup) return1

                if (agroup > bgroup) return  1

                 // … and then within group (when relevant) 

                if (a < b) return1

                if (a == b) return 0

                if (a > b) return  1

            } 


    please explain me how does this works, i printed in console.log() but how the comparison and ordering is happening here..?  Thank you..

    (0) 
  8. Naveen Kumar

    hi,

    I created a table and did bindAggregation on items properties with sorter on one parameter.

    now my table is displayed and sorted as per parameter.
    Now i want to sort the table based on parameter 2 on runtime.
    how can i implement this.

    Thanx

    Naveen

    (0) 
  9. Mike Doyle

    Thanks DJ, you helped me get my binding syntax straight.  What I need to do is sort by document number and then item.  I then need to group by the document number only.  The easiest way I found is just to pass two sorters with only the first having grouping. I declared all of this in my XML view like so (I’m mapping to a JSON model):

    items=”{ path: ‘docs’, parameters: {operationMode: ‘Client’}, sorter: [ { path: ‘DocNo’, descending: false, group: true }, { path: ‘data>ItemNo’, descending: false } ] }”

    (0) 

Leave a Reply