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.
Related
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.
Newish to Backbone, and having some trouble. Going to try to ask this in a generic, no-code way since the application I'm tasked with maintaining is several thousand lines long... hope I can be clear.
I have a method myMethod(), that belongs to a model App.Person.
I have a collection App.PersonList that holds several instances of App.Person.
I have an instance (myPersonList) App.PersonList that I'm creating within an object (myDonationForm) that is an instance of an object App.DonationForm (and here we roam even further outside my comfort zone: App.DonationForm extends an object named Controller which extends an object called Base which seems to be a base.js thing and I have very little idea what's happening here but I hope it doesn't matter for my immediate need).
Also in App.DonationForm, I have an instance (myErrorMsg) of a model App.Errors. I would like to be able to set an attribute of myErrors from myMethod() but can't work out the syntax to refer to myErrors, traversing up the tree of nested objects and then back down a parallel step.
I hope that made sense. To visualize it:
myDonationForm, inst of App.DonationForm, ext Controller
|--myPersonList, inst of App.PersonList, ext Collection
| |--myPerson[1], inst of App.Person, ext Model // I want to change from here
| | +---myMethod()
| |--myPerson[2], inst of App.Person, ext Model // or from here
| +---myMethod()
+myErrorMsg, inst of App.Errors, ext Model // an attribute of this.
Thank you in advance for any pointers you can offer.
Edited to add a code snippet (and I accidentally tried to edit Hoyen's answer, not my own question! didn't realize it til I got the peer review screen, ugh)
App.SpecialDonationForm = App.DonationForm.extend({
[...]
initialize: function(options){
App.DonationForm.prototype.initialize.call(this, options);
[...]
},
start: function(){
App.DonationForm.prototype.start.call(this);
[...]
this.myPersonList = new App.PersonList(this.initialData);
this.myErrorMsg = new App.Errors();
[...]
Basically since myErrorMsg is an instance of a Backbone model you need to use Backbone's model methods to set the attributes. So, it should look something like this:
App.Person = Backbone.Model.extend(
{
[...]
defaults: {
[...],
errorMsg: new App.Errors()
},
[...]
myMethod: function(){
var value = ""; // set it to what ever you like
this.get("errorMsg").set("message",value); // "message", is the attribute you want to update with the value
this.trigger('change:errorMsg'); // not sure if this is needed. but this will insure that this will trigger any event listeners on errorMsg
}
});
App.SpecialDonationForm = App.DonationForm.extend({
[...]
initialize: function(options){
App.DonationForm.prototype.initialize.call(this, options);
[...]
},
start: function(){
App.DonationForm.prototype.start.call(this);
[...]
this.myPersonList = new App.PersonList(this.initialData);
this.myErrorMsg = new App.Errors();
this.myPersonList.on('change:errorMsg',showError.bind(this));
function showError(model,val,options){
this.myErrorMsg.set(_.extend(this.myErrorMsg.defaults,model.toJSON());
}
[...]
An event aggregator may work in this scenario.
Setup to extend Backbone events
App.eventAgg = _.extend({object}, Backbone.Events);
During App.Errors initialize, listenTo an event
this.listenTo(App.eventAgg, 'someEvent', this.doSomeUpdate);
In App.Person's myMethod, trigger the event
App.eventAgg.trigger('someEvent');
To answer your generic question even more generically,
Based on your hierarchy, a Person object doesn't know anything at all about the Errors object. Child objects don't (and shouldn't) know anything about their parents.
A Person object does (presumably) know, however, that an "error" has occurred and someone might want to know about it.
Since the Person object doesn't know any more than that, all it can do is trigger a custom event.
Some other object needs to listen for this custom event. Based on your hierarchy, the only other object that has access to the Person object is the PersonList collection.
The PersonList collection is now in the same situation. It has learned (from the custom event) that an error has occurred, but it can't do anything about it and it doesn't know about its parents. So all it can do is trigger another custom event.
The DonationForm object has to listen for the custom events from the PersonList collection. When it receives the event, it can then pass it to the Errors object.
Voila.
The best way to think of the problem is to consider it in terms of the Backbone TodoMVC app (even though that's not what I'm doing, it's similar in that it has one main view and many list views). If you declare a custom destroy method on the Todo model (such as one I have below) and click on a view to delete it, then in the method that's triggered (where this.model.destroy is called), you can't invoke the custom destroy method on the model, because this.model is not an instance of the model. Somehow calling this.model.destroy
clear: function () { //in the todo app, this method is called clear
this.model.destroy();
}
works to destroy the model but not if you're trying to invoke a custom destroy method.
Below, I explain the same in terms of my app.
I have a Backbone application that uses server side storage but am unable to send a delete request to the server. Using another SO answer, I created a custom destroy method on the model (FunkyModel shown below) and I try to call that custom destroy method in a deleteModel method in the view but to no avail.
However, it's not calling the custom destroy method on FunkyModel.When I inspect this.model, it doesn't say it's an instance of FunkyModel, rather it only says Backbone.Model and then lists the model's properties. So obviously it's not an instance of FunkyModel and therefore not a surprise that it can't call the custom destroy method on FunkyModel, but at the same time under the attributes property, it has all the attributes of an instance of FunkyModel.
Question: how can a model have attributes that are unique to an instance of a model but not be an instance of that model, but rather simply Backbone.Model
Further Info:
The application is structured like the BackboneMVC Todo App in that there is one main view and then it attaches more views as list items. When I click on a delete symbol on one of the views, it triggers an event that calls the deleteModel method, in which I call
deleteModel(e){
this.model.destroy();
}
So naturally, depending on which list view I click on, the attributes of this.model will be different, but in no case is it ever an instance of the model. It's simply a Backbone.Model and its attributes have the properties of a particular instance. I'm not sure how a model that's not an instance can have properties in its attributes that are from a particular instance.
model
export class FunkyModel extends Model{
defaults(){
return {
name: '',
type: ''
}
}
addToCollection(){
collection.add(this);
this.save();
}
destroy(options){
var opts = _.extend({url: '/api/'}, options || {});
console.log("never getting run");
return Backbone.Model.prototype.destroy.call(this, opts);
}
}
view
deleteModel(e){
this.model.destroy({success: function(model, response, xhr){
console.log(model,xhr, options, "success callback") //xhr is undefined
},
error: function(model, xhr,options){
console.log(model,xhr,options, "error callback")
}
}
Further Info
When the model's created, I do
this.model = new FunkyModel();
this.model.set({"type": "funky"})
When the model gets saved, it gets added to the collection (code shown above in original OP). I'm wondering if when I call this.model.destroy(), it's only destroying it in terms of removing it from the collection (hence the success callback is fired).
I'm developing a webapplication with Resthub, so there is a backbone.js stack at the front-side. I need to call a method, everytime a new view (also all sorts of subviews) is rendered, to add some Twitter-Bootstrap specific stuff (help-popovers, kind of quick help, which get their options from a global json file, so the help-texts are easier to maintain).
As far as I know there isn't a backbone-built-in event which is fired every time a view is rendered.
So my question is: What is the easiest way to extend all views, so that they fire an event when the render method is (implicitly or explicitly) called. I want to extend all my views cause I don't want to trigger this event manually in all views I have, because it's error-prone and all developers has to remember that they've to trigger that event.
If you want to do something(fire an event or anything else) for all cases when the render method is called, the most straight forward way might be to update the render method in your copy of Backbone's source code (assuming you want the behavior across the project).
By default the render method just returns 'this'
render: function() {
return this;
},
If there is something you always want to do before render, you can add it within the render method
render: function() {
//add your extra code/call
return this;
},
Alternatively you can also override the prototype of Backbone.View function and update/create your own version(s) something like
_.extend(Backbone.View.prototype, Backbone.Events, {
render: function() {
console.log('This is a test');
return this;
}
});
var testView = Backbone.View.extend({
});
var testview = new testView();
testview.render(); //displays This is a test
//any view rendered will now have the console log
Taking this a step further, you can add your own version of render, calling it say 'myrender' and/or add your own event(s) say 'myevent' which can then be called before/after you call render/myrender
_.extend(Backbone.View.prototype, Backbone.Events, {
render: function() {
//console.log('This is a test');
this.mynewevent();
return this;
},
myrender: function() {
console.log('Pre-render work');
this.render();
},
mynewevent: function() {
console.log('New Event work');
}
});
var testView = Backbone.View.extend({
});
var testview = new testView();
//testview.render();
testview.myrender();
Underscore's extend is being used here and since Backbone has a dependency on Underscore, if you are using Backbone, Underscore should be available for you as well.
in backbone we have an app that uses an event Aggregator, located on the window.App.Events
now, in many views, we bind to that aggregator, and i manually wrote a destroy function on a view, which handles unbinding from that event aggregator and then removing the view. (instead of directly removing the view).
now, there were certain models where we needed this functionality as well, but i can't figure out how to tackle it.
certain models need to bind to certain events, but maybe i'm mistaken but if we delete a model from a collection it stays in memory due to these bindings to the event aggregator which are still in place.
there isn't really a remove function on a model, like a view has.
so how would i tacke this?
EDIT
on request, some code example.
App = {
Events: _.extend({}, Backbone.Events)
};
var User = Backbone.Model.extend({
initialize: function(){
_.bindAll(this, 'hide');
App.Events.bind('burglar-enters-the-building', this.hide);
},
hide: function(burglarName){
this.set({'isHidden': true});
console.warn("%s is hiding... because %s entered the house", this.get('name'), burglarName);
}
});
var Users = Backbone.Collection.extend({
model: User
});
var House = Backbone.Model.extend({
initialize: function(){
this.set({'inhabitants': new Users()});
},
evacuate: function(){
this.get('inhabitants').reset();
}
});
$(function(){
var myHouse = new House({});
myHouse.get('inhabitants').reset([{id: 1, name: 'John'}, {id: 1, name: 'Jane'}]);
console.log('currently living in the house: ', myHouse.get('inhabitants').toJSON());
App.Events.trigger('burglar-enters-the-building', 'burglar1');
myHouse.evacuate();
console.log('currently living in the house: ', myHouse.get('inhabitants').toJSON());
App.Events.trigger('burglar-enters-the-building', 'burglar2');
});
view this code in action on jsFiddle (output in the console): http://jsfiddle.net/saelfaer/szvFY/1/
as you can see, i don't bind to the events on the model, but to an event aggregator.
unbinding events from the model itself, is not necessary because if it's removed nobody will ever trigger an event on it again. but the eventAggregator is always in place, for the ease of passing events through the entire app.
the code example shows, that even when they are removed from the collection, they don't live in the house anymore, but still execute the hide command when a burglar enters the house.
I see that even when the binding event direction is this way Object1 -> listening -> Object2 it has to be removed in order to Object1 lost any alive reference.
And seeing that listening to the Model remove event is not a solution due it is not called in a Collection.reset() call then we have two solutions:
1. Overwrite normal Collection cleanUp
As #dira sais here you can overwrite Collection._removeReference to make a more proper cleaning of the method.
I don't like this solutions for two reasons:
I don't like to overwrite a method that has to call super after it.
I don't like to overwrite private methods
2. Over-wrapping your Collection.reset() calls
Wich is the opposite: instead of adding deeper functionality, add upper functionality.
Then instead of calling Collection.reset() directly you can call an implementation that cleanUp the models before been silently removed:
cleanUp: function( data ){
this.each( function( model ) { model.unlink(); } );
this.reset( data );
}
A sorter version of your code can looks like this:
AppEvents = {};
_.extend(AppEvents, Backbone.Events)
var User = Backbone.Model.extend({
initialize: function(){
AppEvents.on('my_event', this.listen, this);
},
listen: function(){
console.log("%s still listening...", this.get('name'));
},
unlink: function(){
AppEvents.off( null, null, this );
}
});
var Users = Backbone.Collection.extend({
model: User,
cleanUp: function( data ){
this.each( function( model ) { model.unlink(); } );
this.reset( data );
}
});
// testing
var users = new Users([{name: 'John'}]);
console.log('users.size: ', users.size()); // 1
AppEvents.trigger('my_event'); // John still listening...
users.cleanUp();
console.log('users.size: ', users.size()); // 0
AppEvents.trigger('my_event'); // (nothing)
Check the jsFiddle.
Update: Verification that the Model is removed after remove the binding-event link
First thing we verify that Object1 listening to an event in Object2 creates a link in the direction Obect2 -> Object1:
In the above image we see as the Model (#314019) is not only retained by the users collection but also for the AppEvents object which is observing. Looks like the event linking for a programmer perspective is Object that listen -> to -> Object that is listened but in fact is completely the opposite: Object that is listened -> to -> Object that is listening.
Now if we use the Collection.reset() to empty the Collection we see as the users link has been removed but the AppEvents link remains:
The users link has disappear and also the link OurModel.collection what I think is part of the Collection._removeReference() job.
When we use our Collection.cleanUp() method the object disappear from the memory, I can't make the Chrome.profile tool to explicitly telling me the object #314019 has been removed but I can see that it is not anymore among the memory objects.
I think the clean references process is a tricky part of Backbone.
When you remove a Model from a Collection the Collection takes care to unbind any event on the Model that the Collection its self is binding. Check this private Collection method.
Maybe you can use such a same technique in your Aggregator:
// ... Aggregator code
the_model.on( "remove", this.unlinkModel, this );
// ... more Aggregator code
unlinkModel: function( model ){
model.off( null, null, this );
}
This is in the case the direction of the binding is Aggregator -> Model. If the direction is the opposite I don't think you have to make any cleaning after Model removed.
Instead of wrapping Collection's reset with cleanUp as fguillen suggested, I prefer extending Collection and overriding reset directly. The reason is that
cleanUp takes effect only in client's code, but not in library(i.e. Backbone)'s.
For example, Collection.fetch may internally call Collection.reset. Unless modifying the Backbone's source code, we cannot unbind models from events(as in cleanUp) after calling Collection.fetch.
Basically, my suggested snippet is as follows:
var MyCollection = Backbone.Collection.extend({
reset: function(models, options) {
this.each(function(model) {
model.unlink(); // same as fguillen's code
});
Backbone.Collection.prototype.reset.apply(this, arguments);
}
});
Later, we can create new collections based on MyCollection.