Question

Questions:

What is the recommended way to filter a Ext.List populated with hierarchical data from a store? I need to filter out child objects that belongs to a parent object I have selected to filter on. The childs, in this case games should then populate the list.

What I need

  • The Ext.List should be possible to filter by round, i.e. "Omgång 1", "Omgång 2" and so on. ("Omgång" = "Round" in Swedish) When selecting "Omgång 1" as filter the list should only display games from that round in the list. See JSON document below.
  • The list should be grouped by date ("kickOff") and sorted by "gameId" ASC.

What I have done

  • Created an Ext.List populated with data retrived via an Ext.data.Store that gets data from a JSON document read via ReST proxy.
  • The Ext.List reads its data from the store Rounds (see below). The problem is that I only see one game for round "Omgång 1" when there should be 8 games.

This is what I have managed to achieve so far. The list is filtered using the buttons but only one of the list items are shown.

Example of the list and the filters.

EM.model.Round

The model for Round. It has a one-to-many relationship with Match.

Ext.define('EM.model.Round', {
extend: 'Ext.data.Model',

init: function() {},

config: {
    storeId: 'Rounds',

    fields: [
        'name', 
        'lockedDate',
    ],
    associations: { 
        type: 'hasMany', 
        model: 'EM.model.Match', 
        primaryKey: 'gameId',
        name: 'matches',
        autoLoad: true,
        associationKey: 'games'
    }   
},
});

EM.model.Match

The model for Match. It belongs to Round.

Ext.define('EM.model.Match', {
extend: 'Ext.data.Model',

init: function() {},

config: {
    fields: [
        { 
            name: 'gameId', 
            type: 'int'
        }, 
        {
            name: 'firstTeam',
            type: 'string'
        },
        {
            name: 'firstTeamClass',
            type: 'string',

            convert: function(value, record) {
                return util.convertFieldValueToLowerCase('firstTeam', record);
            }
        }, 
        {
            name: 'secondTeam',
            type: 'string'
        },
        {
            name: 'secondTeamClass',
            type: 'string',

            convert: function(value, record) {
                return util.convertFieldValueToLowerCase('secondTeam', record);
            }
        },
        'kickOff',
        {
            name: 'kickOffHour',
            convert: function(value, record) {
                var timestamp = new Date(util.convertUnixTimeToMilliseconds(record.get('kickOff')));

                return Ext.Date.format(timestamp, 'H:i');                   
            }
        },
        { 
            name: 'firstTeamGoals', 
            type: 'int', 
            defaultValue: 0 
         },
        { 
            name: 'secondTeamGoals', 
            type: 'int', 
            defaultValue: 0 
         },          
        { 
            name: 'firstTeamGoalsBet', 
         }, 
        { 
            name: 'secondTeamGoalsBet', 
         },
        'points',
        {
            name: 'pointsEarned',
            convert: function(value, record) {
                var className = 'no-points-earned';
                var points = record.get('points'); 

                if (typeof points == 'undefined') {
                    return '';
                }

                if (points > 0) {
                    className = 'points-earned';
                }

                return '<div class="' + className + '">' + points + '</div>'
            }
        },
    ],

    associations: {
        type: 'belongsTo',
        model: 'EM.model.Round',
        name: 'round',
        autoLoad: true
    }

}
});

EM.store.Rounds

The store that reads the JSON document. This store is then used to populate the Ext.List.

Ext.define('EM.store.Rounds', {
extend: 'Ext.data.Store',

config: {
    model: 'EM.model.Round',
    storeId: 'Rounds',
    filters: [{
        property: 'name',
        value: 'Round 1'
    }],
    /*grouper: {
        groupFn: function (item) {
            //var kickOff = new Date(util.convertUnixTimeToMilliseconds(item.get('kickOff')));
            //return kickOff.format('d mmmm yyyy');
        },
        //sortProperty: 'kickOff'
    },*/        
    proxy: {
        type: 'rest',
        url : 'resources/json/matches.json',
        reader: {
            type: 'json',
        }
    },
    autoLoad: true,
}
});

JSON document

This is the JSON document that is read by the proxy in EM.store.Rounds.

[
{   
    "name": "Omgång 1",
    "lockedDate": 1325420111,
    "games": [
        {
            "gameId": 1,
            "firstTeam": "Pol",
            "secondTeam": "Gre",
            "kickOff": 1339178400,
            "firstTeamGoals": 0,
            "secondTeamGoals": 3,
            "firstTeamGoalsBet": 0,
            "secondTeamGoalsBet": 3,
            "points": 3
        },
        {
            "gameId": 2,
            "firstTeam": "Rus",
            "secondTeam": "Cze",
            "kickOff": 1339188300,
            "firstTeamGoals": 4,
            "secondTeamGoals": 1,
            "firstTeamGoalsBet": 1,
            "secondTeamGoalsBet": 2,
            "points": 0
        },{
            "gameId": 3,
            "firstTeam": "Ned",
            "secondTeam": "Den",
            "kickOff": 1339264800,
            "firstTeamGoals": 2,
            "secondTeamGoals": 1,
            "firstTeamGoalsBet": 4,
            "secondTeamGoalsBet": 2,
            "points": 2
        },
        {
            "gameId": 4,
            "firstTeam": "Ger",
            "secondTeam": "Por",
            "firstTeamGoalsBet": 4,
            "secondTeamGoalsBet": 0,
            "kickOff": 1339274700
        },
        {
            "gameId": 5,
            "firstTeam": "Spa",
            "secondTeam": "Ita",
            "firstTeamGoalsBet": 3,
            "secondTeamGoalsBet": 2,
            "kickOff": 1339351200
        },  
        {
            "gameId": 6,
            "firstTeam": "Irl",
            "secondTeam": "Cro",
            "kickOff": 1339361100
        },
        {
            "gameId": 7,
            "firstTeam": "Fra",
            "secondTeam": "Eng",
            "kickOff": 1339437600
        },
                    {
            "gameId": 8,
            "firstTeam": "Ukr",
            "secondTeam": "Swe",
            "kickOff": 1339447500
        }
    ]
},
{
     "name": "Omgång 2",
     "games": [
         {
            "gameId": 4,
            "firstTeam": "Gre",
            "secondTeam": "Cze",
            "kickOff": 1339524000
        }
     ]
},
{
     "name": "Omgång 3",
     "games": [
         {
            "gameId": 4,
            "firstTeam": "Gre",
            "secondTeam": "Rus",
            "kickOff": 1339869600
        }
     ]
},
{
     "name": "Kvart",
     "games": [
         {
            "gameId": 4,
            "firstTeam": "1A",
            "secondTeam": "2B",
            "kickOff": 1340311500
        }
     ]
},
{
     "name": "Semi",
     "games": [
         {
            "gameId": 4,
            "firstTeam": "#25",
            "secondTeam": "#27",
            "kickOff": 1340829900
        }
     ]
},
{
     "name": "Final",
     "games": [
         {
            "gameId": 4,
            "firstTeam": "#29",
            "secondTeam": "#30",
            "kickOff": 1341175500
        }
     ]
}
]

EM.view.MatchList

The list view that displays the list of matches.

Ext.define('EM.view.MatchList', {
extend: 'Ext.List',
xtype: 'matchlist',

requires: [
    'Ext.TitleBar',
    'EM.store.Rounds'
],

config: {
    id: 'match-list',       
    store: 'Rounds',
    //grouped: true,
    scrollable: false,

    items: [
        {
            xtype: 'titlebar',

            scrollable: {
                direction: 'horizontal',
                directionLock: true
            },

            items: [
                {
                    xtype: 'button',
                    text: 'Omgång 1',
                    handler: function() {
                        var sto = Ext.getStore('Rounds');
                        sto.clearFilter();
                        sto.filter('name', 'Omgång 1');
                        console.log(sto);
                    }
                },
                {
                    xtype: 'button',                        
                    text: 'Omgång 2',
                    handler: function() {
                        var sto = Ext.getStore('Rounds');
                        sto.clearFilter();
                        sto.filter('name', 'Omgång 2');

                    }
                },
                {
                    xtype: 'button',
                    text: 'Omgång 3',
                    handler: function() {
                        var sto = Ext.getStore('Rounds');
                        sto.clearFilter();
                        sto.filter('name', 'Omgång 3');

                    }
                },
                {
                    xtype: 'button',                        
                    text: 'Kvart',
                    handler: function() {
                        var sto = Ext.getStore('Rounds');
                        sto.clearFilter();
                        sto.filter('name', 'Kvart');

                    }
                },
                {
                    xtype: 'button',                        
                    text: 'Semi',
                    handler: function() {
                        var sto = Ext.getStore('Rounds');
                        sto.clearFilter();
                        sto.filter('name', 'Semi');

                    }
                },
                {
                    xtype: 'button',                        
                    text: 'Final',
                    handler: function() {
                        var sto = Ext.getStore('Rounds');
                        sto.clearFilter();
                        sto.filter('name', 'Final');

                    }
                }                           

            ],
        },
        {
            xtype: 'panel',
            html: 'Senast uppdaterad: Idag kl 20:12'
        }
    ],

    itemTpl: [

        '<div class="match-meta-data">',
        '<tpl for="matches">',          
            '<div class="team-wrapper home-team">{firstTeam} <div class="flag {firstTeamClass}"><span></span></div> <span class="goals-scored">{firstTeamGoals}</span></div>',
            '<div class="kick-off-time">{kickOffHour}</div>',
            '<div class="team-wrapper away-team"><span class="goals-scored">{secondTeamGoals}</span> <div class="flag {secondTeamClass}"><span></span></div> {secondTeam}</div>',
            '<div class="bet-meta-data">',
                '<img class="user-icon" src="resources/images/user-22x26.png" />',
                '<div class="home-team goals-bet">{firstTeamGoalsBet}</div>',
                '<div class="away-team goals-bet">{secondTeamGoalsBet}</div>',
                '{pointsEarned}',   
            '</div>',
        '</tpl>',
        '</div>',

        ].join('')
    },

});

This is my first Sencha Touch app, so feel free to point out any bad practices that you see in the code. Could someone please provide an example of how to achieve what I'm aiming for? I have spent a lot of time trying to figure this one out.

The full project can be downloaded from GitHub at https://github.com/eriktoyra/EM-Tipset. Latest branch is _filter-match-list.

Was it helpful?

Solution 2

I managed to find a solution to this problem by using a test scenario that resembles the actual one I have.

Solution

What I did was:

  1. Let the Division model handle the proxy and read the data from match.json instead of having the Division store read the data.
  2. Setup relationships between the Division and Team models so they are aware of each other.
  3. Setup two stores. One for Division and one for Team. When loading the Division store I used a callback to populate the Team store with Team data from the Division store.
  4. Then I populated the list with the Team store, which has Team objects which now is aware of their references to the Division model/store.
  5. The actual filtering is done by looking up the Division object for each Team item in the list and compare the name property of Division with the one provided by the filter.

The full solution is provided below and also available on GitHub.

I will keep the question as unanswered for some days if someone else should find a better solution or point out some improvements.

    /**
     * @description The main purpose of this mockup is to test how to filter a list using hierarchical data. The code is adapted from the example 
     * "Filtering data in an Ext.List component in Sencha Touch 2" by Peter deHaan.  
     * @see <a href="http://senchaexamples.com/2012/03/15/filtering-data-in-an-ext-list-component-in-sencha-touch-2/">Filtering data in an Ext.List component in Sencha Touch 2</a>.
     * @author <a href="mailto:erik.toyra[at]gmail.com">Erik Töyrä</a>      
     */
    Ext.application({
        launch: function () {
            /**
             * Division model
             */
            Ext.define("Division", {
                extend: 'Ext.data.Model',
                config: {
                    fields: [
                        'division'
                    ],
                    // Setup a hasMany relations between Division and Team
                    hasMany: {model: 'Team', name: 'teams'},

                    // Load data from teams.json into the Division model
                    proxy: {
                        type: 'rest',
                        url : 'teams.json',
                        reader: {
                            type: 'json'
                        }
                    }
                }
            });

            /**
             * Team model
             */
            Ext.define("Team", {
                extend: 'Ext.data.Model',
                config: {
                    fields: [
                        'name', 'league'
                    ],
                    // Setup a belongsTo relationship between Team and Division
                    belongsTo: {model: 'Division'},
                }
            });                

            /**
             * Division store
             */
            var divisionStore = Ext.create('Ext.data.Store', {
                model: "Division",
                storeId: 'divisionStore',
            });

            /**
             * Team store
             */
            var teamStore = Ext.create('Ext.data.Store', {
                model: "Team",
                storeId: 'teamStore', // This is the store we will reference in our Ext.list below.
                fields: ['division', 'leage', 'name'],
            });

            /**
             * Load the Division store which holds all of the data read from teams.json. 
             * After the data has been loaded we add the team data to the teamStore.
             */
            divisionStore.load({
                callback: function() {
                    // Loop through each division and retrieve the all teams that resides as 
                    // childs to the division. Then we add each team to the teamStore.
                    divisionStore.each(function(division) {
                        division.teams().each(function(team) {
                            teamStore.add(team);
                        });
                    });
                }
            });

            /**
             * Create the list that should be filtered by Division and display a filtered list with Team objects.
             */
            Ext.create('Ext.List', {
                fullscreen: true,

                items: [{
                    xtype: 'titlebar',
                    docked: 'top',
                    ui: 'neutral',

                    items: [{
                        text: 'West only',
                        handler: function () {
                            return util.doFilter('West');
                        } // handler
                    }, {
                        text: 'Central only',
                        handler: function () {
                            return util.doFilter('Central');
                        } // handler
                    }, {
                        text: 'East only',
                        handler: function () {
                            return util.doFilter('East');                               
                        } // handler
                    }, {
                        text: 'Clear filters',
                        ui: 'decline',
                        align: 'right',
                        handler: function () {
                            Ext.getStore('teamStore').clearFilter();
                        } // handler
                    }
                    ] // items (toolbar)
                }], // items (list)
                store: 'teamStore',
                itemTpl: '{name}, {league}',
            }); // create()


            /**
             * Utility functions
             */
            var util = (function() {
                var util = {};

                /**
                 * Filter the teamStore by the passed in Division name.
                 */
                util.doFilter = function(filterOption) {
                    var store = Ext.getStore('teamStore');

                    // Clear all existing filters first...
                    store.clearFilter();

                    // ... then apply the selected filter
                    store.filterBy(function(record, id) {
                            return record.getDivision().get('division') == filterOption;
                        }, this);
                }

                return util;
            })();
        } // launch
    }); // application()

JSON data

[
{
    division: 'East',
    teams: [
        {
            name: 'New York Yankees',
            league: 'AL',
        }, {
            name: 'Tampa Bay',
            league: 'AL',
        }, {
            name: 'Boston',
            league: 'AL',
        }, {
            name: 'Toronto',
            league: 'AL',
        }, {
            name: 'Baltimore',
            league: 'AL',
        }
    ]
},
{
    division: 'Central',
    teams: [
        {
            name: 'Detroit',
            league: 'AL',

        }, {
            name: 'Cleveland',
            league: 'AL',
        }, {
            name: 'Chicago White Sox',
            league: 'AL',
        }, {
            name: 'Kansas City',
            league: 'AL',
        }, {
            name: 'Minnesota',
            league: 'AL',
        }                            
    ]
},
{
    division: 'West',
    teams: [
    {
            name: 'Texas',
            league: 'AL',
        }, {
            name: 'Los Angeles Angels',
            league: 'AL',
        }, {
            name: 'Oakland',
            league: 'AL',
        }, {
            name: 'Seattle',
            league: 'AL',
        }
    ]
}
]

OTHER TIPS

These are really two problems. I don't know why your entire store doesn't seem to render in the list view; some stepping and pausing might shed more light on that. If you have a demo hosted somewhere, I'd be happy to look at it.

Regarding your multiple round filtering issue, I can think of two things right now:

  • As far as I can tell, performing a filter action on a store causes it to "lose" data i.e. throw out all data that doesn't match the filter (rather than hide it from accessors). If you're loading all of your rounds in the same server call, into one store, I'm not sure filtering is the right thing for you to do. I might be wrong about this, but I just tried filtering a store in one of my applications from the Javascript console; when I applied a filter that didn't match any of the items, the store emptied. Based on this maybe filtering isn't the best thing to do here.

  • Create a separate listview for each round, each using the same common Rounds store. Use the itemTpl config and an XTemplate to decide which matches should go into each list. Use the Omgång buttons to swap out these lists from the viewport, as appropriate.

Update answer after better understanding your problem and code

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top