What approach would be more efficient?
I have a Backbone.Collection so i Create a Backbone.View to render this collection. The CollectionView render method:
render: ->
container = document.createDocumentFragment()
#collection.each (item) ->
view = new ItemView(item)
container.appendChild view.el
view.render()
$(el).append container
I can use the events in two forms.
1.- Set the events object in CollectionView, so i need to declare the action of select an item in the CollectionView and "rescue" the model that i selected.
CollectionView extends Backbone.View
events:
'click #itemView', 'onSelectItem'
onSelectItem: ->
##Get the model
##Show ItemDetailView
2.- Set the events object per itemView, so the select method don't need to retrieve the model.
ItemView extends Backbone.View
events:
'click #div','onSelect'
onSelect: ->
#Show ItemDetailView
Which of this options are better?
EDITED: I create a JSperf snippet http://jsperf.com/backbone-events-on-collectionview-or-per-itemview
JSperf show us that the ItemView approach is more faster, but is this the only metric of importance?
The second approach is much better. Performance aside, the code is much more straightforward. In 6 months, if you had to return to this code, would you think the event would be handled in the ItemView, or the CollectionView? It is a click event on the ItemView, so that's where I would go to look for how it is handled.
Is there a good reason to handle the event in the collection? If you needed the handle the event from the CollectionView, you could delegate the event to the collection. A little redirection, but, to me, this is far clearer. For example:
# The collection view
initialize: ->
#listenTo #, 'selected', #itemSelected
render: ->
container = document.createDocumentFragment()
#collection.each (item) ->
view = new ItemView(item, parent: #)
container.appendChild view.el
view.render()
$(el).append container
itemSelected: (model)->
# Do whatever you need to here, like
# show the ItemDetailView in the container
# The ItemView
ItemView extends Backbone.View
events:
'click #div','onSelect'
onSelect: ->
#options.parent.trigger('selected', #model, #)
#Show ItemDetailView
If I understand you correctly, option 2 seems the most sane. You will be referring to this.model as opposed to digging through the collection to find the model associated with the clicked view.
Related
Below is the code I currently have that re-renders a collectionView on every addition or removal of a model. However, it seems inefficient as it has to render the whole thing every time, when all I really need is one modelView to be removed or one to be added. So how could I achieve this?
var CollectionView = Marionette.CollectionView.extend({
childView: ModelView,
initialize: function() {
[ "add", "remove" ].forEach(function(eventName) {
this.listenTo(this.collection, eventName, this.render, this);
}.bind(this));
}
});
Thanks in advance for any help you can give!
This is already done automatically in Marionette:
When a model is added to the collection, the collection view will
render that one model in to the collection of item views.
When a model is removed from a collection (or destroyed / deleted),
the collection view will close and remove that model's item view.
I'm using Backbone with Coffeescript in an app. Now the example that I'll use is made as trivial as possible. I have a header in my app that all the views share it. This header has a logout link with #logout index. Now what I'm trying to do is to put the events method and the method that the event will use it when it is triggered in a mixin object and extend the prototype of my Backbone View. Here is the code:
Mixin.Header =
events: ->
'click #logout': 'logout'
logout: (e)->
e.preventDefault()
$.ajax(
type: 'DELETE'
url: '/logout'
).then(
->
document.location = ''
)
class View extends Backbone.View
initialize: (options)->
_.extend(View.prototype, Mixin.Header)
I've been looking through the Backbone source code and I simply can't find the problem why this is not working. The events get delegated to the View through the delegateEvents() method and when the View is initialized the initialize method is being called first.
From the fine manual:
events view.events or view.events()
[...]
Backbone will automatically attach the event listeners at instantiation time, right before invoking initialize.
You're trying to add events in initialize but the events are bound before initialize is called.
You could call delegateEvents yourself to rebind the events after you've updated the prototype:
initialize: (options)->
_.extend(View.prototype, Mixin.Header)
#delegateEvents() # <----------
but the structure would be a little weird because you'd be modifying the class inside an instance.
I think you'd be better off modifying the class before you have any instances:
class View extends Backbone.View
_.extend(View.prototype, Mixin.Header)
or you could use the CoffeeScript shortcut for prototype:
class View extends Backbone.View
_.extend(View::, Mixin.Header)
# -----------^^
or even:
class View extends Backbone.View
_.extend(#::, Mixin.Header)
You will still run into problems if View has its own events as _.extend blindly overwrites properties rather than merging them. You'd need something smarter than _.extend to properly handle this and it would need to be able to figure out how to merge events functions and objects.
It's hard to me to explain what I'm looking for, so I will start with code.
I have Marionette view like this:
Marionette.ItemView.extend({
model: new Models.Cards(),
template: 'poker/cards',
events: {
'click player': 'playerClicked'
},
playerClicked: function( e ) {
// THIS WORKS!
}
}
How can I do something like this:
events: {
'click player': 'playerClicked',
'render player': 'playerRendered'
},
so that playerRendered be called when <player> is rendered?
If you want to run some code when the ItemView itself is rendered, use onRender:
Marionette.ItemView.extend({
// ...
onRender: function() {
console.log("Rendered the ItemView!")
}
//...
})
Marionette doesn't have built-in events for parts of an ItemView being rendered.
#joews is right. If you want to react to a specific piece of your ItemView being rendered you should react on the render event for the whole ItemView. When you render an ItemView it renders the whole template in memory and then appends it to the DOM as one piece. If you want to render a collection of models then you can use a collection view and each ItemView will be rendered independently.
If you want an example of setting up views with CollectionViews and CompositeViews check out this fiddle of rendering four hands of five cards, all the cards having random values: http://jsfiddle.net/kjdgygy5/
From the Backbone.js (1.2.3) documentation:
add collection.add(models, [options])
Add a model (or an array of models) to the collection, firing an "add"
event for each model, and an "update" event afterwards.
In my code I want to add a new model (visually a card with an input) to the collection and then force focus on the input of created card. I'm not sure if it's the correct way, but I basically listen to the event fired when model is added to collection and trigger another event which facilitates focusing from the view of created model:
# ItemView
initialize: ->
App.vent.on "focus:field", =>
$("div.card:last").addClass("edit")
#$el.find("input:first").focus()
When I listen to the update event, it works as expected: :last card (the new model) is selected and input is focused.
But when I listen to the add event, initialize function fires on the penultimate model, instead of the new model. The new model is still created, but the edit class and focus is forced on the one before.
Why is that happens?
I would use an update event for this purpose, but unfortunately for me model.destroy method also fires an update event, so it results in ruined UI, and if I pass model.destroy with silient:true bad things happen overall. Is there a workaround?
Relevant code:
# CompositeView
class List.Models extends App.Views.CompositeView
template: "path/to/template"
childViewContainer: "div.destination"
childView: List.Model
events:
"click #add-model": "addModel"
initialize: ->
#listenTo #collection, "update", ->
App.vent.trigger "focus:field"
addModel: (e) ->
#$el.find("#add-model").prop "disabled", true
model = App.request "new:model:entity"
#collection.add(model)
# ItemView
class List.Model extends App.Views.ItemView
template: "path/to/template"
initialize: ->
App.vent.on "focus:field", =>
$("div.card:last").addClass("edit")
#$el.find("input:first").focus()
Edit:
Apparently the difference between add and update events is that the add event is fired immediately after #collection.add(model) is called, but before new model is inserted in the DOM, resulting in $("div.card:last") selector pointing to the penultimate view. I'm not sure however, maybe more experienced people can clarify whether this is true or not. I've come to this conclusion after adding timeout to the execution of the App.vent:
initialize: ->
delay = (ms, func) -> setTimeout func, ms
# Triggered via add event
App.vent.on "focus:field", =>
delay 100, =>
$("div.card:last").addClass("edit")
$("input:first").focus()
I'm trying to understand what the best practice is for communicating between the different components of a Backbone project. I've been re-implementing the Backbone version of TodoMVC and my question is around removing models from the collection object.
I understand that Backbone model instances trigger a change event whenever any of its model properties are modified via .set() and that the view associated with the model instance should .listenTo() changes from the model instance and then re-render. However, what's the best practice for communication between models and the collection object that houses it? For example, how should the communication work when a model is removed from the collection object?
Here's what I think: when a model is removed, I think the model instance should emit a custom event that the collection object is listening for and pass itself along. When the collection object hears this event, it should remove the model instance from the list (along with any event listeners attached to the model) and then the entire collection object should re-render itself. This re-rendering process will then create a new set of models and model-views.
Is this the best approach? I would love to hear your input! To me, this process of re-rendering sounds really expensive since you'll have to destroy the existing DOM elements, remove their event listeners, and then re-create them again.
UPDATE - 3/26/2015
To make this more concrete, I'll include the code that I have so far and point out where I feel my understanding is off.
File Structure
collections
a. todoList.coffee
models
a. todo.coffee
views
a. todoView.coffee
b. todoListView.coffee
app.coffee
app.coffee
window.app = app = window.app || {}
data = [
{
title: 'Eat dinner',
completed: false
}
{
title: 'Go to gym',
completed: true
}
]
app.todos = data.map (todo) -> new app.Todo todo
app.todoList = new app.TodoList app.todos
app.todoListView = new app.TodoListView
collection: app.todoList
app.$app = $('#todo-app')
$('#todo-app').append app.todoListView.render().el
todo.coffee
window.app = app = window.app || {}
app.Todo = Backbone.Model.extend
defaults:
title: ''
completed: false
toggleComplete: ->
this.set 'completed', !this.get 'completed'
todoList.coffee
window.app = app = window.app || {}
app.TodoList = Backbone.Collection.extend
model: app.Todo
initialize: () ->
# This is what I don't like - creating 'remove-todo' event
this.on 'remove-todo', this.removeTodoFromList
removeTodoFromList: (model) ->
this.remove model
getCompleted: ->
this.filter (model) -> model.completed
getNotCompleted: ->
this.filter (model) -> !model.completed
todoView.coffee
window.app = app = window.app || {}
app.TodoView = Backbone.View.extend
tagName: 'li'
events:
'click input' : 'checkComplete'
'click .delete' : 'removeTodo'
checkComplete: (e) ->
this.model.toggleComplete()
removeTodo: (e) ->
# I don't like how the collection is listening for this custom event 'remove-todo'
this.model.trigger 'remove-todo', this.model
initialize: ->
this.listenTo this.model, 'change:completed', () ->
this.render()
render: ->
template = _.template $('#todo-view').html()
this.$el.html template this.model.toJSON()
return this
todoListView.coffee
window.app = app = window.app || {}
app.TodoListView = Backbone.View.extend
tagName: 'ul'
className: 'todo-list'
initialize: ->
this.collection.on 'remove', (() ->
this.resetListView()
this.render()
), this
addOne: (model) ->
todoView = new app.TodoView
model: model
this.$el.append todoView.render().el
resetListView: ->
this.$el.html('')
render: ->
_.each this.collection.models, ((model) -> this.addOne model), this
return this
Explanation of Code
As you can see in my comments above, whenever a click happens on the remove button, my todoView triggers a custom event 'remove-todo'. The todoList collection listens to this event and removes the specific model from the collection. Since a 'remove' event is triggered whenever a collection removes a model, the todoListView listens for this 'remove' event and then re-renders. I feel like I'm off somewhere. Any advice?
It seems you are talking about views when you talk about models. When a model is removed from a collection you don't need a custom event, a "remove" event is triggered.
http://backbonejs.org/#Collection-remove
If you want to remove the corresponding view, use http://backbonejs.org/#View-remove this will manage the DOM and listener.
The re-rendering of view (I don't understand what you are talking about when you talk about rerendering collection) can be triggered if you listen to "remove" models from the collection, otherwise listen to http://backbonejs.org/#Collection-add, or http://backbonejs.org/#Model-hasChanged is you want to rerender your view when a model has changed.
I hope it helps.