Search code examples
javascriptbackbone.jsunderscore.jsmarionettefrontend

Marionette CollectionView not re-rendering after collection.fetch


I have an 'email' style app that displays messages grouped by the date. When the app loads, a shallow collection of messages are fetched and loaded into a backbone collection. Each model in the collection represents a list of messages within a grouping. The MessageGroup represents a group of messages and the MessagesView displays the groups of messages.

This all works well until the collection is fetched again like after a filter is applied, only the group headers are displayed, not the messages inside. I've tried triggering an event that the MessagesView can listen for, then re-render itself but I get an error: listening.obj.off is not a function.

var MessageModel = Backbone.Model.extend({});
var MessageCollection = Backbone.Collection.extend({
    model: MessageModel
});

var GroupModel = Backbone.Model.extend({});
var GroupCollection = Backbone.Collection.extend({
    model: GroupModel,
    url: '/messages/recipient',
    parse: function (response) {

        // Create a grouped JSON to render nested views with
        var messageArray = [];
        var groupedlist = _.groupBy(response.messages, function(model) {
            return model.publishDate;
        });

        _.forEach(groupedlist, function(n, key) {
            var grouping = {};
            grouping.group = key;
            grouping.list = n;
            messageArray.push(grouping);
        });

        return messageArray;
    },
    fetchMessages: function() {
        this.fetch({
            data: filtermodel.toJSON(),
            success: function() {

                var messagecollection = new MessageCollection();

                // Loop through each grouping and set sub-collections
                groupcollection.each(function(group) {
                    var list = group.get('list');

                    messagecollection.reset(list);
                    group.set('list', messagecollection);
                });
            }
        });
    }
});

// Model to track applied filters
var FilterModel = Backbone.Model.extend({
    defaults: {
        folder: 0
    }
});

// ------------------------  VIEWS  ------------- //

// View for a single Message
var MessageView = Backbone.Marionette.ItemView.extend({
    template: require('../../../templates/activities/message-item.ejs'),
    events: { 'click li.item': 'getMessageDetail' },
    getMessageDetail: function(e){
        this.triggerMethod('showDetail', this.model);
        //initMessageDetail(this.model);
    }

});

// Grouped container view for a list of Messages within a group
var MessageGroup = Backbone.Marionette.CompositeView.extend({
    template: require('../../../templates/activities/message-list.ejs'),
    className: "list-view-group-container",
    childView: MessageView,
    childViewContainer: "ul.viewcontainer",
    initialize: function() {
        this.collection = this.model.get('list');

    }

});

// Top level view for all grouped messages
var MessagesView = Backbone.Marionette.CollectionView.extend({
    childView: MessageGroup,
    initialize: function() {
        this.collection.on('change', this.log, this);
    },
    log: function() {
        console.log('triggered log');
    }
});

// View for selected message detail
var MessageDetailView = Backbone.Marionette.ItemView.extend({
    template: require('../../../templates/activities/message-detail.ejs'),
    className: "message-content-wrapper"
});

// View for filter selection bar
var MessageFilterView = Backbone.Marionette.ItemView.extend({
    template: require('../../../templates/activities/message-filter-bar.ejs'),
    events: {
        'click #search-btn': function() {
            filtermodel.set('search', $('#search-input').val());
            groupcollection.fetchMessages();
        }
    }
});


var filtermodel = new FilterModel();
var groupcollection = new GroupCollection();

// Fetch messages first run
groupcollection.fetchMessages();


// LayoutView to display in center panel of application
module.exports = ViewMessages = Marionette.LayoutView.extend({
    template: require('../../../templates/activities/viewmessages.ejs'),
    className: 'content full-height',
    regions: {
        'messagelistregion': '#messageList',
        'messagedetailregion': '.message-detail',
        'messagefilterregion': '.filter-bar'
    },
    childEvents: { 'showDetail': 'onMessageSelected' },
    onMessageSelected: function (childView, childViewModel) {

        var that = this;

        var detailModel = childViewModel.clone();
        var messageDetailView = new MessageDetailView({model:detailModel});
        that.messagedetailregion.show(messageDetailView);
    },
    onShow: function(){

        var that = this;
        var messagesview = new MessagesView({
            collection: groupcollection
        });

        var messageFilterView = new MessageFilterView();
        that.messagelistregion.show(messagesview);
        $("#messageList").ioslist();

        that.messagefilterregion.show(messageFilterView);
        this.messagedetailregion.on('show', function() {
            console.log('message detail region shown:' + that.messagedetailregion.currentView);
        })
    }
});

I'm thinking its because the work that is done to build out the groupings of messages inside the success callback doesn't finish before the reset event is triggered and the view is refreshed. How can I get the MessagesView to update after subsequent fetches?

UPDATE: I moved the post-success logic of grouping the collection into its hierarchical tree/leaf structure to a custom event (fetchSuccess) in the top level collectionview (MessagesView):

var MessagesView = Backbone.Marionette.CollectionView.extend({
    childView: MessageGroup,
    initialize: function() {
        this.collection.on('fetch:success', this.fetchSuccess, this);
    },
    fetchSuccess: function() {
        var messagecollection = new MessageCollection();

        groupcollection.each(function(group) {
            var list = group.get('list');

            messagecollection.reset(list);
            group.set('list', messagecollection);
        });
    }
});

It is being triggered in the success callback of the fetch. I'm pretty sure this is a good way of rendering the collection, but I cant seem to get around the error in Marionette:

**Uncaught TypeError: listening.obj.off is not a function**

Anyone have any ideas why this collectionview will not re-render??


Solution

  • I was able to determine that the creation of the models in the collection occurred after the reset event, but before the structure of the nested models was built out:

    success: function() {
    
        var messagecollection = new MessageCollection();
    
        // Loop through each grouping and set sub-collections
        groupcollection.each(function(group) {
            var list = group.get('list');
    
            messagecollection.reset(list);
            group.set('list', messagecollection);
        });
    };
    

    After any filter event, grouping, sorting etc, the collection structure needs to be modified into this nested hierarchy each time. The view was picking up the reset event before the structure was built out so the child views had no data to render. I fixed this by cloning the original collection after the changes and having the views render the cloned collection:

    groupcollection.fetch({
        reset: true,
        data: filtermodel.toJSON(),
        success: function() {
    
            groupcollection.each(function(group) {
                var list = group.get('list');
    
                var messagecollection = new MessageCollection(list);
                group.set('list', messagecollection);
            });
    
            filteredcollection.reset(groupcollection.toJSON());
        }
    });