0

I have a marionette compositeview which I am using to create a item list for a profile page on an app. For the child view, I extend from an already existing ItemView.

When I use this.setElement(this.el.innerHTML) in the compositeview onRender function, all the events set in the child view no longer are triggered and even more so, triggering them in the console on the inspector tool in the browser, does nothing.

However when I do not use setElement, the container div is added to my markup, but now all the events in the child view work.

Can someone help me understand this please.

The Collection I am using has a custom clone method.

I am using a global collection which is updated and stored in cache on each fetch.

When I actually instantiate my view, the collection has already been used and a region in the main layout view has been populated with a item list similar to the one I want to render.

This is how I instantiate my view:

var currentUser = Profile.get('username');

        // Perform changes to global collection
        Items.url = API + '/items/search?q=' + currentUser + '&size=20';

        Items.parse = function (response) {

            if (!response.results) {
                return response;
            } else {
                return response.results;
            }
        };

        Items.fetch(
            {success: function (collection, response, options) {

                this.listOfItems = new View.itemListProfilePage({
                    template: TemplIds.profilePagePostedItems,
                    parentClass: 'profile-cols',
                    collection:  Items, // global collection
                    filterAttr: {user: currentUser},
                    isFiltered: true,
                    lazyLoad: true,
                    childViewContainer: '#profile-items',
                    childView: View.itemProfilePage.extend({
                        template: TemplIds.item
                    })
                });

                Backbone.trigger('main:show', this.listOfItems); //'main:show' is an event in layoutview which calls region.show

            },
                remove: false
        });

My compositeview:

View.itemListProfilePage = Marionette.CompositeView.extend({
    collection: null,       //original collection cloned later for filtering
    fetch: null,            //promise for fetched items
    lazyView: null,

    options: {
        parentClass: '',
        filterAttr: {},
        isFiltered: false,
        lazyLoad: false
    },

    initialize: function () {

        this.stopListening(this.collection);

        //Change collection property and re-apply events
        this.collection = this.collection.clone(this.options.filterAttr, this.options.isFiltered);
        this._initialEvents();

        this.collection.reset(this.collection.where(this.options.filterAttr), {reset: true});

        this.listenTo(Backbone, 'edit:profileItems', this.addEditClassToSection);

    },

    onRender: function () {

        this.setElement(this.el.innerHTML, true);

    },

    onShow: function () {

        if (this.options.parentClass) {
            this.el.parentElement.className = this.options.parentClass;
        }
    },

    addEditClassToSection: function (options) {

        if ( options.innerHTML !== 'edit' ) {
            this.el.classList.add('edit-mode');
        } else {
            this.el.classList.remove('edit-mode');
        }
    },
}

The parent ItemView:

View.Item = Marionette.ItemView.extend({
    model: null,
    numLikes: null,             //live set of DOM elements containing like counter
    modalItem: null,            //view class with further details about the item to be used within a modal       

    events: {
        'click img.highlight': 'showModal'
    },

    initialize: function (options) {
        var itemWithHeader;     //extended item view class with header at the top and no footer
        var addToCart;

        //Set up all like-related events
        this.listenTo(this.model, "change:numLikes", this.updateNumLikes);
        this.listenTo(this.model, "change:liked", this.updateLiked);

        //Set up the view classes to be used within the modal on click
        itemWithHeader = View.ItemWithHeader.extend({
            template: this.template,
            model: this.model               //TODO: move to inside itemDetails
        });
        itemAddToCart = View.ItemAddToCart.extend({
            template: TemplIds.itemAddCart,
            model: this.model               //TODO: move to inside itemDetails
        });
        this.modalItem = View.ItemDetails.extend({
            template: TemplIds.itemDetails,
            model: this.model,
            withHeader: itemWithHeader,
            addToCart: itemAddToCart
        });
    },

    onRender: function () {
        var imgContainerEl; 
        var likeButtonEl;

        //Get rid of the opinionated div
        this.setElement(this.el.innerHTML);
        this.numLikes = this.el.getElementsByClassName('num');

        //Add the like button to the image
        likeButtonEl = new View.LikeButton({
            template: TemplIds.likeButton,
            model: this.model
        }).render().el;
        this.el.firstElementChild.appendChild(likeButtonEl);    //insert button inside img element
    },

    showModal: function (evt) {

        var modalView = new View.Modal({
            views: {
                'first': {view: this.modalItem}
            }
        });

        Backbone.trigger('modal:show', modalView);
    },
});

The itemView for each individual item in my list:

View.itemProfilePage =  View.Item.extend({

    events: _.extend({},View.Item.prototype.events, {
            'click .delete-me': 'destroyView'
        }
    ),

    onRender: function () {

        View.Item.prototype.onRender.call(this);

        this.deleteButtonEl = new View.itemDeleteButton({
            template: TemplIds.deleteButton
        }).render().el;

        this.el.firstElementChild.appendChild(this.deleteButtonEl);

    },

    destroyView: function (evt) {

        this.model.destroy();

    }

});
hyprstack
  • 4,043
  • 6
  • 46
  • 89

1 Answers1

0

The short answer is that you should not be using setElement.

Backbone specifically uses the extra container div to scope/bind the view's events. When you use setElement you are changing what the parent element is. Since you are doing this in the onRender function, which is called after the template has been rendered and the events have already been bound, you are losing your event bindings.

The correct thing to do if you are going to use Marionette and Backbone is to expect and utilize the "extra" div wrapper that is generated when you render a view. You can take control of the markup for that "wrapper" div by using className, id, and tagName view properties on your view classes.

Andrew Hubbs
  • 9,338
  • 9
  • 48
  • 71
  • By using `className`, `id` and `tagName` doesn't this defeat the purpose of using templates? Also shouldn't `setElement` be alright in theory, as it is a `Backbone` function. Do you know of any other workaround for this? – hyprstack Sep 22 '15 at 08:58
  • Can I render a `compositeView` without setting a template? Something like for an `itemView` where you set `template: false` – hyprstack Sep 22 '15 at 09:17
  • Hmm, I don't think I agree with either comment. Templates are used to generate the markup inside a view, those view properties are used to markup the container. Yes, you *can* use `setElement`, but you need to understand the consequences that has with Marionette. If you want to use `setElement` then you should call it prior to calling `render`, not as part of the `onRender` function. – Andrew Hubbs Sep 22 '15 at 15:11
  • A `CompositeView` without a template is basically a `CollectionView` (but it still has the container div). Take a look at that if you think you don't need to template. – Andrew Hubbs Sep 22 '15 at 15:12