Get last item view from CompositeView in Marionette - backbone.js

Is it possible to get the last ItemView from a Backbone CompositeView? I've found a lot of documentation for getting the last model in a Collection, but not that last View in a Collection of Views.
The reason I would like to do this is so I can render the last row in a table slightly differently.
The following is the code I'm using right now, it works fine, but it would be less "hacky" if I could get the correct ItemView from the CompositeView that created and rendered it. It uses jQuery to search the entire part of the DOM contained by the CompositeView for the last element, then manipulates that element.
B.ListControl.View = Backbone.Marionette.CompositeView.extend({
itemView: ...,
itemViewContainer: ...,
template: ...,
// ON RENDER
onRender: function(){
// Add button to the last element in the list
this.$el.find('div:last').text('Custome stuff goes here');
}
});
Thanks!

When your collection is fetched you can get last item in this way:
this.children.findByIndex(this.children.length - 1);
Babysitter plugin provides a lot of useful methods for you:
findByModel, findByCollection, findByCustom, findByIndex, findByCid

Related

Prevent itemView from being added to CompositeView after collection "add"

I have a problem with Backbone and Marionette. I have a CompositeView with a collection where people can a comment, this all works nicely, the comment is added and saved to the server but I don't want the view to update and to show the newly added comment. I have tried this:
App.Views.CommentsView = Backbone.Marionette.CompositeView.extend({
template: '#article-comment-container',
itemViewContainer: 'ul',
itemView: App.Views.CommentView,
collectionEvents: {
"add": "modelAdded"
},
modelAdded: function(){
console.log('Please do nothing!');
}
});
But the item is still rendered into the page on top of my modelAdded function being called. Can I prevent that from happening at some point?
In a different scenario I would like new items to be added to the top of the list and not the bottom. Do I have to override the entire appendHtml method achieve this?
Setting the collection event add simply adds another handler to the queue for that event; it doesn't replace any other events so the default marionette behaviour will still occur.
I assume you're calling the create method on the collection to create your new comment model. If this is the case you simply need to set the silent option to true. Now the add event will not fire and Marionette will not create and render the view for that model. You can do it like this:
commentCollection.create(commentModel, {silent: true});
As for you second question about prepending, yes I would override appendHtml method. Or to keep the method names consistent with what actually happens, create a method called prependHtml and then override the renderItemView method to call prependHtml.

Marionette CollectionView with one unique ItemView

Is there a way I can create a single unique view within a Marionette CollectionView? I mean something like this:
CollectionView
StartView (this is the unique view)
Regular ItemView
Regular ItemView
Regular ItemView
etc.
By unique, I mean the "Start ItemView" must use a unique template, and not have the methods associated with the regular ItemViews.
Things I've tried:
Just prepending the start view to the el of the CollectionView is not desirable, because my CollectionView's methods act on its child views.
In the Marionette documentation, overriding buildItemView is suggested for this situation (I think?). I'm not sure how to override it. Just calling buildItemView doesnt work, because the StartView doesn't have an associated model or any options. Seems like I can't leave these args out, or it just returns undefined.
I cant include the markup for a StartView in my CollectionView's template because... Marionette CollectionViews cant have a template.
From the Marionette documentation, it doesn't seem like this fits the use case for the CompositeView. But maybe I'm wrong.
I'm probably missing something fairly obvious. Thanks in advance for the help.
edit: formatting
Create 2 different views, 'StartView' and 'NormalView':
StartView = Marionette.ItemView.extend({
template:"#start-template'
});
NormalView = Marionette.ItemView.extend({
template:"#normal-template"
});
In your collection view, override getItemView and return your 'StartView' and 'NormalView'... something like this:
MyCollectionView = Marionette.CollectionView.extend({
getItemView:function(item){
if(item.get("ImTheFirst")===true)
return StartView;
else
return NormalView;
}
});
On the first item in your collection, add a property 'ImTheFirst' (for example) and set it to true.
What you are looking for here is a CompositeView. A CompositeView is a mash-up between an ItemView and a CollectionView. Essentially, the CompositeView itself would act as your StartView, and then you define an ItemView to act as your "Regular ItemView"'s.
Something like this:
RegularItemView = Backbone.Marionette.ItemView.extend({
template: "#regular-itemview-template"
});
CompositeView = Backbone.Marionette.CompositeView.extend({
itemView: RegularItemView,
template: "#start-template"
});

Re-Rendering Handlebars partial from backbone view

I have a view, which holds handlebars template.
that template consist of another partial template.
that partial template holds a list of results, which i am using in different parts of my app.
anyhow, when trying to filter the results, i'd like to render only that part. meaning the backbone view should not render the whole view just the partial.
can it be done?
Yes, it's possible. The easiest way is to execute the whole template as you do when rendering the complete view, but only replace the the part you need in the view's el.
Something like:
template: Handlebars.compile(templateHtml),
render: function() {
//let's say your render looks something like this
this.$el.html(this.template(this.model.toJSON());
},
renderList: function() {
var html = this.template(this.model.toJSON());
var selector = "#list";
//replace only the contents of the #list element
this.$el.find(selector).replaceWith($(selector, html));
}
Depending on how dynamic your template is, you may have to call this.delegateEvents() after replacing the list for the view's events to work correctly.
Edit based on comments:
To clarify, the method I propose here does execute the view's main handlebars template again, but it doesn't render the whole view again.
Step by step:
Execute the Handlebars template function as you do in normal render.
var html = this.template(this.model.toJSON());
The variable html now contains a string of HTML markup. Nothing has yet been rendered.
Define a selector for the element, which you would like to re-render.
var selector = "#list";
Find the DOM element to replace. This presumes that you have already rendered the view once. Otherwise there will be no #list element within this.$el.
this.$el.find(selector)
Find the corresponding element in the templated html string, and replace the existing element with the new one:
.replaceWith($(selector, html));
This will only replace the #list element that's currently on the page. Anything outside #list will not be re-rendered or touched in any way.
The main reason I propose you do it this way instead of executing and rendering the partial template separately is that your view doesn't need to know anything about the implementation details of the template and the templating engine. All it needs to know that there is an element #list. I believe this is a cleaner solution, and keeps your template details separate from your view logic.

Backbone Marionette emptyView template being added to the DOM twice

I'm use a Marionette Composite View with the emptyView property to render a simple template when the collection for the composite view has no models. emptyView works fine on multiple places in my app, but for some reason there's one view where the emptyView is rendering twice on the page.
My initial thought was the the view was re-rendering and not removing itself when the collection was synced. A console.log in the initialize function reveals that initialize is only being called once though.
class AllLists extends Backbone.Marionette.CompositeView
itemView: List
emptyView: NoLists
template: AllListsTemplate
className: 'lists'
initialize: (options) ->
#collection.fetch()
console.log 'lists ', #collection, #model
class NoLists extends Backbone.Marionette.ItemView
template: NoListsTemplate
tagName: 'li'
className: 'no-lists'
As you can see, there's nothing too crazy going on here. The empty list template is just a simple h4 tag with some text in it.
Any ideas as to what might be causing this?
This is a known bug in the v0.9.3 release, and is fixed in the up-coming v0.9.4 release.
The fix is currently in the dev branch as a release preview, if you would like to get it now https://github.com/derickbailey/backbone.marionette/tree/dev
And there are a few pull requests that provided fixes if you want to patch your version with code from one of them:
https://github.com/derickbailey/backbone.marionette/pull/175

Correct way to insert a view with Backbone.js

I have a simple backbone.js app. I want to render a view into the DOM of the HTML page, this view is a detail view for a model. My HTML page already has the DIV element that I want to render the view into. If I try to render my view like this:
detailView = new RulesPanelView({model : #model})
$("#detail").html(detailView.render().el)
It fails and I get [Object HTMLDivElement] inserted into the DOM, not my rendered HTML.
This is the only way I can get it to work and it seems like a hack:
$("#detail").html('')
detailView = new RulesPanelView({model : #model})
$("#detail").append(detailView.render().el)
Having to empty the HTML of the DIV before rendering so I don't get multiple views rendered inside #detail which is what would happend with append.
Also aren't I creating way too many views this way, just seems cleaner to replace the HTML as in the first code segment?
What is the correct way to render this view?
What you want is to pass the already inserted DOM node to the view as a 'el' option to the constructor:
new RulesPanelView({el: $("#detail")});
This way, it won't render again. You still need to make sure your view's 'render' method will be able to render a correct view from an updated model, though.
The backbone documentation mentions this as a good way to avoid rendering too much stuff at once.
I actually append in the render method of the view. This doesn't work if you want to re-render when models change - but for that I've added a refresh method that render actually calls before appending. I then bind the refresh to the model change (if I need that). So in my View, I do this:
render: function(){
var markup = this.refresh();
$(markup).appendTo('#some-selector');
return this;
},
refresh: function(){
return $(this.el).html($.mustache(this.template, this.model.toJSON()));
},
Not sure if that's the "best", but I think it works pretty well. I've also seen where you have a collection bound to a view that loops through all of the models and renders "sub-views" of the collection view - this provides a nicer programmatic approach than hard-coding where you're going to append.

Resources