I have a context problem / design problem for my Backbone view.
Goal
The user selects a user from a list / user collection in a separate view.
The mentioned view passes an global event that the editUserView receives ("edit-contact").
The editUserView should receive this event and extract the (user) model.id attribute. By using this model.id I want to update the view with the corresponding object retrieved from the existing view model Tsms.Collection.Users.
Problem
The context passed to the updateView function is wrong, and thus I do not have access to the parent views .render() function. The debugger states "render() is not a function".
Since the context is not that of the parent view I am also unable to set the this.current variable.
How would I go about solving this problem?
View code
Tsms.Views.editUserView = Backbone.View.extend({
model: Tsms.Collections.Users,
initialize: function(options) {
Tsms.require_template('edituser')
this.template = _.template($('#template_edituser').html());
this.current = -1;
Tsms.vent.on('edit-contact', this.updateView)
},
updateView: function(model) {
this.current = model.id;
this.render();
},
render: function() {
this.$el.html(this.template(this.model.get(this.current).attributes));
return this;
}
});
Backbone's on actually takes three arguments:
on object.on(event, callback, [context])
[...]
To supply a context value for this when the callback is invoked, pass the optional last argument: model.on('change', this.render, this) or model.on({change: this.render}, this).
The easiest and (currently) most idiomatic way to solve your problem would be to use the third context argument:
Tsms.vent.on('edit-contact', this.updateView, this);
While mu is too short is right, you should use Backbone's listenTo to avoid memory leaks (zombie views).
this.listenTo(Tsms.vent, 'edit-contact', this.updateView);
The context is automatically set to this, the calling view.
When remove is called on the view, stopListening is called and any references kept for events are removed.
Another reason to avoid on is that it should be the view that is responsible for the events it wants to handle, the event bus shouldn't have to know.
Related
I am new to Backbone.js
I must be doing something wrong here. I'm trying to make a little demo to see what I can do with Backbone and basing it off some sample code.
I an get "Uncaught TypeError: Cannot call method toJSON of undefined".
I see why it is doing this, because the .bind("change", taskView.render) call is setting the context to the model (which the alert confirms) but it seems like there should at least be an argument to the render function to get access to the view. Maybe I am just going about it the wrong way? (see the sample code below).
task.bind("change", _.bind(taskView.render, taskView));
On Backbone Views and Models, the 'this' context for 'bind' is the calling object, so for model.bind('change', ...) this with be the model. For view.bind(... this will be the view.
You are getting the error because task.bind("change", _.bind(taskView.render, taskView)); sets this to be task when render runs, but this actually needs to be taskView.
Since you want to bind the model event to the view, as irvani suggests, the best way to do this is to use the .listenTo method (see: http://backbonejs.org/#Events-listenTo)
taskView.listenTo(task, 'change', taskView.render);
Depending on how you want the view lifecycle to work, you could also put the code in the initialize method of the view, which executes when the view is created. Then use stopListening in the remove method, to clear up the listener when the view is no longer in use.
As the task model is passed into the view, you get a fairly neat:
AskView = Backbone.Model.extend({
initialize: function(){
this.listenTo(this.model, 'change', this.render);
},
...
remove: function(){
this.stopListening(this.model);
...
}
});
var askView = new AskView({ model: task });
However, the one line solution to your problem is:
task.on("change", taskView.render, taskView);
bind is just an alias of on (see: http://backbonejs.org/#Events-on). The first argument is the event you are listening to, the second is the method to fire, and the third argument is the context to bind to, in this case, your taskView.
task.listenTo(model, 'change', taskView.render);
This says that listen to the model change and render the taskView on every change.
As irvani suggested, use listenTo instead.
object.listenTo(other, event, callback) Tell an object to listen to a particular event on an other object. The advantage of using this
form, instead of other.on(event, callback, object), is that listenTo
allows the object to keep track of the events, and they can be removed
all at once later on. The callback will always be called with object
as context.
In your case,
taskView.listenTo(task,"change",taskView.render);
assuming taskView is Backbone.View and task is a Backbone.Model.
The chances of memory leaks will be less when you use listenTo compared to using on.
If you must use on, you can specify the context as a third argument as and mu is too short suggested:
To supply a context value for this when the callback is invoked, pass the optional last argument: model.on('change', this.render, this) or model.on({change: this.render}, this).
In your case:
task.on("change", taskView.render, taskView);
I'm using Backbone and I have the following model and collection
App.Models.Person = Backbone.Model.extend({
});
App.Collections.People = Backbone.Collection.extend({
model: App.Models.Person,
url: 'api/people',
});
However what I'm struggling on is the best way to render this collection. Here's how I've done it so far which works but doesn't seem to be the most elegant solution
App.Views.PeopleView = Backbone.View.extend({
el: $(".people"),
initialize: function () {
this.collection = new App.Collections.People();
//This seems like a bad way to do it?
var that = this;
this.collection.fetch({success: function(){
that.render();
}});
},
render: function () {
var that = this;
_.each(this.collection.models, function (item) {
that.renderPerson(item);
}, this);
},
I'm fairly new to Backbone but have to assign this to a different variable to I use it inside of the success function just seems like a bad way of doing things? Any help on best practices would be appreciated.
Backbone allows you to register for events that you can react to. When the collection is synchronized with the server, it will always fire the sync event. You can choose to listen for that event and call any given method. For instance ...
initialize: function () {
this.collection = new App.Collections.People();
this.listenTo(this.collection, "sync", this.render);
// Fetch the initial state of the collection
this.collection.fetch();
}
... will set up your collection so that it would always call this.render() whenever sync occurs.
The docs on Backbone Events are succinct but pretty good. Keep in mind a few things:
The method you use to register event listeners (i.e. listenTo or on) changes how you provide the context of the called function. listenTo, for instance, will use the current context automatically; on will not. This piece of the docs explains it pretty well.
If you need to remove a view, you will need to disconnect event listeners. The easiest way to do that is to use listenTo to connect them in the first place; then when destroying the view you can just call view.stopListening().
For rendering, there are a lots of suggestions for how to do it. Generally having a view to render each individual model is one way. You can also use Backbone.Collection#each to iterate over the models and control the scope of the iterating function. For instance:
render: function() {
this.collection.each(function(model) {
var view = new App.Collections.PersonView({ model: model });
view.render();
this.$el.append(view.$el);
}, this);
}
Note the second argument to .each specifies the scope of the iterator. (Again, have a look at the docs on controlling scope. If you'd rather have a framework help out with the rendering, check out Marionette's CollectionView and ItemView.
If your view is supposed to just render the collection you can send the collection to temnplate and iterate through in template, otherwise you can create another subView for that purpose or send the individual models of the collection to another subview and append to the container, hope it was helpful.
I have a Marionette application with a large number of views. I want to log a debug message to the console when each is rendered (and possibly, in the future, on other events)
I am currently doing this by logging from each View's onRender method:
MyModule.MyViewType = Marionette.ItemView.extend({
// ... view properties and methods
onRender: function() {
// ... other onRender code
console.debug('MyModule.MyViewType %s %s', this.cid, 'render');
}
});
This works, but it has several disadvantages:
The logging code must be added manually to each view.
Several views don't have custom onRender actions so I'm adding methods for the purpose of debugging only. That feels wrong.
If I want to alter or remove the logging methods completely (e.g. to go to production), I need to alter a lot of code.
If I want to add code for another event, e.g. show, I will need to add an event handler or a new method to every view.
Is there a way to log every View render without adding code to each View?
Yes. You can decorate Backbone.View.constructor to hook into the View creation lifecycle. You can then register callbacks for any event on all View instance..
!function() {
// Development: log view renders and other events
// Don't execute this function in production.
// Save a reference to the original Backbone.View and create a new type
// that replaces the constructor method
var OriginalView = Backbone.View,
LoggingView = OriginalView.extend({
constructor: function() {
// Execute the original constructor first
OriginalView.apply(this, arguments);
// Allow views to define a `type` property to clarify log messages
var type = this.type || 'Unknown View Type',
cid = this.cid;
// Bind to Marionette.View's `render` event (and any other events)
this.listenTo(this, 'render', function(e,b) {
console.debug('%s %s - %s', type, cid, 'render');
});
}
});
// Replace Backbone.View with our decorated view
Backbone.View = LoggingView;
}();
To log view types, add a property to your View implementations:
MyModule.MyViewType = Marionette.ItemView.extend({
type: 'MyModule.MyViewType',
// ... rest of the view code
});
Reliably determining a JavaScript's object "type" (constructor name) is problematic, so adding this property is the best approach to determine the type of view that is being rendered.
This answer is easily generalised to multiple event types by providing an array of the events you want to be logged:
var events = ['render', 'show', 'beforeClose'];
events.forEach(function(eventType) {
this.listenTo(this, eventType, function() {
console.debug('%s %s - %s', type, cid, eventType)
});
}).bind(this);
Example output:
Projects.ViewType1 view13 - render
Projects.ViewType2 view3 - render
Projects.ViewType3 view6 - render
Projects.ViewType4 view9 - render
Projects.ViewType4 view17 - render
Projects.ViewType4 view19 - render
Projects.ViewType2 view3 - render
This approach solves all of the problems described in the question - there is a a small amount of code, Views don't need altering directly and there is a single function call that can be omitted to remove logging in production code.
This approach is specific to Marionette - because vanilla Backbone's render method is user-defined there is no equivalent of the render event.
For more detail on extending Backbone constructor methods see Derick Bailey's blog.
Analyzing this code I am not sure what is actually happening.I keep falling into this trap with JS especially with callbacks. Here is an example taken from backbone's documentation.
//creates a new constructor function with a promptColor function as an attribute.
var Sidebar = Backbone.Model.extend({
promptColor: function() {
var cssColor = prompt("Please enter a CSS color:");
this.set({color: cssColor});
}
});
// creates a property on the global window object called sidebar
window.sidebar = new Sidebar;
// .on is an event listener and passed a callback function taking the parameters of model and color. Here is my confusion, what does it do with the model parameter?
sidebar.on('change:color', function(model, color) {
$('#sidebar').css({background: color});
});
sidebar.set({color: 'white'});
sidebar.promptColor();
My main question is what does it do with the model parameter? What is it actually doing with the model parameter?
Thanks!
In your particular case the model parameter is of no real use since their is a 1-to-1 relationship between the change event and the model.
However, there are times when this is not the case. For example, imagine you have a backbone collection of models. You can attach a "change" event listener to the collection which will get called every time any model in the collection changes. In cases like this, it's helpful to know which model originated the "change" event.
I have a UserPanel view, which uses a UserModel as its model. In the template for the UserPanel, I have a conditional that checks whether or not the model is undefined. If the model exists, it displays user information. If it doesn't, it displays the "registration" form.
On the user information part of the UserPanel, I have what's essentially an "unregister" button. A user clicks it, and the user information is deleted. The UserPanel responds by re-rendering, allowing them to register a different UserModel.
Common sense tells me to call this.model.destroy. When I use this method, my model is deleted from my data store, but the object still exists in this.model. When the view responds to the model update (by calling render), it still thinks it has a valid model, with all its data and the like. I can call delete on this.model, but that doesn't trigger any events. I can't trigger the event before I delete, because then the view updates before I can delete the model. I have setup the view to respond to model deletions with a method that simply uses delete on the model, then calls render. This works, but bothers me on a conceptual level, since those methods are for handling view updates and not more model manipulation.
In general, what's the proper way to explicitly dispose of a model that is not stored by a collection?
EDIT: I am using Backbone.localStorage as my data store. This might have something to do with it.
Instead of binding to the destroy event I would use the success callback of the model.destroy like so:
this.model.destroy({ success: _.bind(this.onModelDestroySuccess, this) });
Then rename your modelDestroyedView to onModelDestroySuccess:
onModelDestroySuccess: function () {
delete this.model;
this.render();
},
I would also define a cleanupModelEvents method that cleans up your event bindings to the model:
cleanupModelEvents: function() {
this.stopListening(this.model);
},
And call that from onModelDestroySuccess:
onModelDestroySucess: function () {
this.cleanupModelEvents();
delete this.model;
this.render();
},
Hope this helps.
this.model.destroy();
and then on the destroy event
this.model = null;