Sunday, March 3, 2013

Secondary sort in dgrid

In my never ending attempt to harvest google search clicks help people in need, today I'm going to explain a way to get a secondary sort working in dgrid, something my google searches could not find a good solution for. I needed this because I was making a grid that could be sorted by several headings as well as being filtered. Everything worked fine for small data sets, but I guess due to the way that dgrid's in-memory store works, sometimes I would apply a filter and the order of the rows would get switched up a bit. The results were still sorted correctly, but the order of the rows that all contained identical values for the column being sorted changed randomly. For example, suppose I'm sorting a movie list by genre:

Then I apply a filter for movies made in Nigeria, the results would still be sorted by genre, but the order of all the movies in the same genre got mixed up:


You can see that in the original list, the Nigerian Action movies showed up in the order 43-8-4-13, but after the results were filtered, the order was 4-8-13-43. The best solution I thought to fix this would be to sort primarily by genre, and secondarily by ID. Since the ID is unique across all the rows, the order of movies that contain identical genres will stay the same.

Let's start with the some HTML and JavaScript to create the filterable grid, there are several tutorials online about how to get this far, so I won't go into any details.
<html>
<head>

<title>dgrid secondary filtering</title>
<script data-dojo-config="async: true" src="js/dojo/dojo.js"></script>
<script>
    require([
        "dgrid/OnDemandGrid", 
        "dojo/store/Memory", 
        "dojo/on", 
        "dojo/dom", 
        "dojo/domReady!"
        ], function(Grid, Memory, On, Dom) {

        var genres = ['Comedy','Action','Drama', 'Musical','Doco'];
        var countries = ['Australia','USA','Japan','England','Nigeria'];
        var data = [];
        for (var i=1; i<50; i++) {
            var title = "Movie " + i;
            var genre = genres[Math.floor(Math.random() * genres.length)];
            var country = countries[Math.floor(Math.random() * 
                countries.length)];
            data.push({id:i, title:title, genre:genre, country:country});
        }

        store = new Memory({ data: data });         
        grid = new Grid({
            store: store,
            query: filterData,
            columns: {id:"ID",title:"Title",genre:"Genre",country:"Country"}
        }, "grid");

        grid.set('sort', [ 
            { attribute : 'genre', descending : false },
            { attribute : 'id', descending : false }
        ]); 
        
        On(Dom.byId("country-select"), "change", function(evt) {
            grid.refresh();
        });
        On(Dom.byId("genre-select"), "change", function(evt) {
            grid.refresh();
        });

        function filterData(item, index, items) {
            var countrySel = Dom.byId("country-select");
            var country = countrySel.options[countrySel.selectedIndex].value;

            var genreSel = Dom.byId("genre-select");
            var genre = genreSel.options[genreSel.selectedIndex].value;

            return (!country || country == item.country) &&
                    (!genre || genre == item.genre);
        }
    });
</script>

<style>
    body { font-family: sans-serif; font-size: 11px; }
    .claro .ui-state-default.dgrid-row-odd { background: #EEE; }
</style>
</head>
<body class="claro">
<div>
    <label for="genre-select">Genre:</label>
    <select id="genre-select">
        <option value="">All</option>
        <option value="Comedy">Comedy</option>
        <option value="Action">Action</option>
        <option value="Drama">Drama</option>
        <option value="Musical">Musical</option>
        <option value="Doco">Doco</option>
    </select>
    <label for="country-select">Country:</label>
    <select id="country-select">
        <option value="">All</option>
        <option value="Australia">Australia</option>
        <option value="England">England</option>
        <option value="India">India</option>
        <option value="Japan">Japan</option>
        <option value="Nigeria">Nigeria</option>
        <option value="USA">USA</option>
    </select>
</div>
<div id="grid">
</div>
</body>
</html>


Although it's not too easy to find in the API, it is possible to sort a grid by multiple columns. For example, we could add this code to set up a default sort which would use a secondary sort on ID to retain the order of rows after filtering:
grid.set('sort', [ 
    { attribute : 'genre', descending : false },
    { attribute : 'id', descending : false }
]);

This works for the default sort, but if we try to sort by country and filter by genre, we run into the same problems. Luckily, dgrid allows you to intercept the sort event and override it with your own implementation by using:
grid.on("dgrid-sort", onSort);

We can then prevent the usual sort for happening by calling preventDefault/stopPropagation on the event passed to our sort function, and then use the sort information from the event, but add our own secondary sort option:
function onSort(event) {
    // Stop the normal sort event/bubbling
    event.preventDefault();
    event.stopPropagation();

    updateSort(event.sort[0]);
}

function updateSort(sortType) {
    // Always sort secondarily by ID so that a definite order is kept 
    grid.set('sort', [
        sortType,
        { attribute : 'id', descending : false }
    ]); 
}


And there you go, sort order maintained, mission accomplished!

Here is the full code:
<html>
<head>


<title>dgrid secondary filtering</title>
<script data-dojo-config="async: true" src="js/dojo/dojo.js"></script>
<script>
    require([
        "dgrid/OnDemandGrid", 
        "dojo/store/Memory", 
        "dojo/on", 
        "dojo/dom", 
        "dojo/domReady!"
        ], function(Grid, Memory, On, Dom) {

        var genres = ['Comedy','Action','Drama', 'Musical','Doco'];
        var countries = ['Australia','USA','Japan','England','Nigeria'];
        var data = [];
        for (var i=1; i<50; i++) {
            var title = "Movie " + i;
            var genre = genres[Math.floor(Math.random() * genres.length)];
            var country = countries[Math.floor(Math.random() * 
                countries.length)];
            data.push({id:i, title:title, genre:genre, country:country});
        }

        store = new Memory({ data: data });         
        grid = new Grid({
            store: store,
            query: filterData,
            columns: {id:"ID",title:"Title",genre:"Genre",country:"Country"}
        }, "grid");

        grid.on("dgrid-sort", onSort);
        updateSort({ attribute : 'genre', descending : false });
        
        On(Dom.byId("country-select"), "change", function(evt) {
            grid.refresh();
        });
        On(Dom.byId("genre-select"), "change", function(evt) {
            grid.refresh();
        });

        function filterData(item, index, items) {
            var countrySel = Dom.byId("country-select");
            var country = countrySel.options[countrySel.selectedIndex].value;

            var genreSel = Dom.byId("genre-select");
            var genre = genreSel.options[genreSel.selectedIndex].value;

            return (!country || country == item.country) &&
                    (!genre || genre == item.genre);
        }

        function onSort(event) {
            // Stop the normal sort event/bubbling
            event.preventDefault();
            event.stopPropagation();

            updateSort(event.sort[0]);
        }

        function updateSort(sortType) {
            // Always sort secondarily by ID so that a definite order is kept 
            grid.set('sort', [
                sortType,
                { attribute : 'id', descending : false }
            ]); 
        }
    });
</script>

<style>
    body { font-family: sans-serif; font-size: 11px; }
    .claro .ui-state-default.dgrid-row-odd { background: #EEE; }
</style>
</head>
<body class="claro">
<div>
    <label for="genre-select">Genre:</label>
    <select id="genre-select">
        <option value="">All</option>
        <option value="Comedy">Comedy</option>
        <option value="Action">Action</option>
        <option value="Drama">Drama</option>
        <option value="Musical">Musical</option>
        <option value="Doco">Doco</option>
    </select>
    <label for="country-select">Country:</label>
    <select id="country-select">
        <option value="">All</option>
        <option value="Australia">Australia</option>
        <option value="England">England</option>
        <option value="India">India</option>
        <option value="Japan">Japan</option>
        <option value="Nigeria">Nigeria</option>
        <option value="USA">USA</option>
    </select>
</div>
<div id="grid">
</div>
</body>
</html>

5 comments: