Backbone Marionette emptyView template being added to the DOM twice - backbone.js

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

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.

Deleting a backbone collection both from the view and the server

Struggling to make the following code work in a view that manages a backbone collection:
class VGStream.Views.Scenarios.ScheduleRisks extends Backbone.View
template: JST['backbone/templates/scenarios/schedule_risks']
initialize: (options) ->
super
Backbone.pubsub.on 'allSchedulesRemoved', #removeAllScheduleRisks, #
removeAllScheduleRisks: =>
for risk in #risks.models
risk.destroy
#render()
render: ->
super
#risks.fetch()
#$el.html #template #
#
I am just showing the relevant code snippet from the Coffeescript class view. This does not delete the 'risk' models either from the database or the list view itself. How should I do it? The event allSchedulesRemoved fires correctly when all schedules are removed. I want the associated risks to be removed as well from both the view as well as the database.
You don't always have to call super
There are built-in #risks.each() method to loop through collections.
Put fetch() in render() is not recommended.
To clear the collection, you could do several #risks.remove(risk) or, after destroyed everything, reset the collection with an empty object.
risk.destroy actually send a delete request to your backend, so whether it's deleted in server or not depends on your backend code.

What is the best practice to use a jQuery Plugin with Backbone.js

I am using jQuery UI with a rails application using backbone.js. I want to make a draggable element? Where do I have to put this function :
$('.area-tools').draggable({handle: ".grap-area", "containment" : "parent"})
Is it on the view? After the render function? Because, the initialize function doesn't find my element, I think the DOM is not already created?
So, i did this :
class Myapp.Views.Tools extends Backbone.View
template: JST['pdfs/tools']
tagName: "div"
className: "pdf-tools"
events:
'click div.rect' : 'drawRect'
initialize: ->
#previewWrapper = $('.preview')
#count = 0;
#
render: ->
$(#el).html(#template())
#initColorPicker()
this
initColorPicker: ->
$('.area-tools').draggable({handle: ".grap-area", "containment" : "parent"})
drawRect: (event) =>
newElement = $('<div id="resizable" class="resizable"><div class="close">x</div><input type="text" name="text_' + #count++ + '" /></div>');
#previewWrapper.append(newElement);
newElement.draggable().resizable();
Is it good? Any recommendation?
I just had the same issue come up when integrating the timeago plugin into my rails/backbone.js app.
My solution was almost the same as yours, except that instead of applying the plugin to the entire document, I apply it just to the view element. i.e. add a this before your selector:
initColorPicker: ->
#.$('.area-tools').draggable({handle: ".grap-area", "containment" : "parent"})
That keeps the range of what you're doing with the plugin confined to the specific view you call the plugin from, which is important.
Your analysis is correct, that your code will not work unless your view is attached to the Dom. You do have multiple options now:
Move the event handling into the View via the events option. I would then also recommend to attach the event handlers to your view.
Attach the view to the dom during creation via the el option. Read this post, especially the section on "Decouple Views from other DOM elements"
Following shioyama's recommendation, is the 3rd option to fix your problem.
All 3 of those should fix your problem. But all 3 are best practice, so you might want to apply all 3.

Can Marionette.Layouts have dynamic content outside sub-regions?

I'm developing a single-page view for a resource that contains multiple nested resources. Using the following template, I can get the top-level attribute or collection to render, but not both:
h1= #name
ul
#decisions
class Happenator.Views.ShowHappening extends Backbone.Marionette.Layout
template: "happenings/show"
regions:
decisions: "#decisions"
initialize: ->
#decisionsView = new Happenator.Views.Decisions(collection: #model.get("decisions"))
# Uncomment to render #model.name, but lose the decisions
# #bindTo(#model, "change", #render)
onRender: ->
#decisions.show(#decisionsView)
Is there an accepted way to bind the top-level layout to re-render when data changes/arrives, or is all dynamic content support to go into sub-regions?
A Layout renders the DOM elements that the regions will manage. So, calling render on the layout again will forceably re-render the region's elements. The regions will see this and pick them up again, but this will have various other negative impacts on your app as you've noted.
The simple way to handle this is to either:
a) have individual change:attribute event handlers that only update the data that changed
b) use a data-binding solution to do this for you, such as https://github.com/theironcook/Backbone.ModelBinder
A third alternative would be to create a 2nd region within your Layout, and have an ItemView populate that region, with the model. Then you could re-render that view whenever the model changes:
h1= #name
ul
#decisions
class Happenator.Views.HeaderView extends Backbone.Marionette.ItemView
template: "happenings/model-template"
initialize: ->
#bindTo(#model, "change", #render)
class Happenator.Views.ShowHappening extends Backbone.Marionette.Layout
template: "happenings/show"
regions:
header: "#header",
decisions: "#decisions"
initialize: ->
#decisionsView = new Happenator.Views.Decisions(collection: #model.get("decisions"))
#headerView = new Happenator.Views.HeaderView(model: #model)
onRender: ->
#decisions.show(#decisionsView)
#header.show(#headerView)
If it were me, I'd go with option #3 and nest another itemview inside of the layout, for the model rendering. This would give you a lot more freedom and flexibility within that view, keeping it separated from the layout.

Backbone.js - where does code to create a new post go?

I'm trying to make a quick sample Backbone.js app that has a Post model, a PostList collection, and a PostView + PostListView. Something simple where you can post in a form and it will add your post to the list of posts.
When someone clicks submit on the post form, it triggers an event in "PostListView", the view for the PostList collection. Where do I create a new post model and add it to the collection? Do I write this code in the View itself? Or does the view call a collection method that does this? Can you even write custom collection methods? If so, how do I call them from a view?
Coming from a Rails background, I naturally edge towards putting code in the collections/models rather than views (rails controllers), but I can't figure out how to call custom collection events from views.
Code is as below. Thanks so much for any help!
PostListView.coffee:
class forum.PostListView extends Backbone.View
tagName: 'section'
className: 'post-list'
events:
'click .post-form button': 'submit'
initialize: ->
#causes the view to render whenever the collection's data is loaded
#collection.bind 'reset', #render
#collection.bind 'add', #render
render: =>
$(#el).html JST['postList']()
$postList = this.$('.post-list')
#iterates through posts, renders, appends to <ul>
#collection.each (post) =>
view = new forum.PostView
model: post
collection: #collection
$postList.append view.render().el
return this
submit: ->
console.log "submitted!"
#collection.trigger 'newPost', this.$('.post-form textarea').val()
PostList.coffee:
class forum.PostList extends Backbone.Collection
model: forum.Post
url: '/posts'
initialize: ->
this.bind 'newPost', newPost
newPost: (postText) ->
console.log "Collection method called!!"
# post = new forum.Post
# content: postText
# #add post
You call a method in PostList, from the PostListView, to add a new model in the collection. Something like this in regular js:
this.collection.add( new forum.Post( this.$( ".post-form textarea").val() ) );
I don't do CoffeeScript so I guess the syntax for above in cs would be like this:
#collection.add new forum.Post
this.$( ".post-form textarea").val()
If it's more complicated to add a new Post to PostList, you could add a method for it in PostList
this.collection.complexAdd( params... );
You add custom methods to PostList in the place where you create the class.. you have done so with the newPost method already.
This is much simpler than your collection listening to view events because views are initialized last.
Edit: I guess it could also come down to opinion, some people prefer models/collections knowing about which backbone views are using them, but in a situation where there are more than one view for one model (for example, TabContent view and TabSelector view for a single TabModel), it makes stuff more complicated.
The following tips may help you...
It might separate things better if you have a separate view for NewPost, whose single responsibility is to manage the new post form. Your PostListView can create that and append it to itself.
Normally, you do not want to add a new model to a collection until after it is saved. So what you may do is give your NewPostForm a reference to the collection and have it add it once it is saved.
You may also want the PostList to dump and recreate the NewPost view after that, and get it ready to add a subsequent post.
Backbone "views" are sometimes more like controller actions in Rails. So creating models, moving them around, saving them, etc. are all perfectly ok to do in view code.

Resources