Overwriting events hash for inherited Backbone.js View - javascript

I have a Backbone View for a general element that specific elements will be inheriting from. I have event handling logic that needs to be applied to all elements, and event handling logic that is specific to child-type elements. I'm having trouble because a child View has its own callback for an event that is also handled by the parent View, and so when I try to use the events hash in both, either the child or parent callback is never called. Let me illustrate with some code:
var ElementView = Backbone.View.extend({
events: {
"mouseup": "upHandler",
"mousedown": "downHandler",
"mousemove": "moveHandler"
},
initialize: function() {
// add events from child
if (this.events)
this.events = _.defaults(this.events, ElementView.prototype.events);
this.delegateEvents(this.events);
}
});
var StrokeView = ElementView.extend({
events: {
"mousemove": "strokeMoveHandler"
}
});
How would I solve this in an extensible way, especially if I will later have another level of inheritance?

One way to handle this would be to use "namespaces" for events:
var ElementView = Backbone.View.extend({
events: {
"mouseup.element": "upHandler",
"mousedown.element": "downHandler",
"mousemove.element": "moveHandler"
},
initialize: function() {
// add events from child
if (this.events)
this.events = _.defaults(this.events, ElementView.prototype.events);
this.delegateEvents(this.events);
}
});
var StrokeView = ElementView.extend({
events: {
"mousemove.strokeview": "strokeMoveHandler"
}
});
In fact, this approach is suggested in Backbone.js documentation.

I've done something similiar by taking advantage of JavaScript's faux-super as mentioned in the Backbone.js documentation and the initialization function
var ElementView = Backbone.View.extend({
events: {
"mouseup": "upHandler",
"mousedown": "downHandler",
"mousemove": "moveHandler"
},
initialize: function() {
this.delegateEvents();
}
});
var StrokeView = ElementView.extend({
initialize: function() {
this.events = _.extend({}, this.events, {
"mousemove": "strokeMoveHandler"
});
// Call the parent's initialization function
ElementView.prototype.initialize.call(this);
}
});
var SubStrokeView = StrokeView.extend({
initialize: function() {
this.events = _.extend({}, this.events, {
"click": "subStrokeClickHandler",
"mouseup": "subStrokeMouseupHandler"
});
// Call the parent's initialization function
StrokeView.prototype.initialize.call(this);
}
});
var c = new SubStrokeView();
console.log(c.events);
// Should be something like
// click: "subStrokeClickHandler"
// mousedown: "downHandler"
// mousemove: "strokeMoveHandler"
// mouseup: "subStrokeMouseupHandler"
The magic happens by setting the events within the initialize function. If you have multiple events attributes in your prototypes, JavaScript will only see the one set most recently due to how prototyping works.
Instead, by doing it this way, each view sets its own this.events, then calls its parent's initialize function which in turn extends this.events with its events, and so on.
You do need to set this.events in this specific way:
this.events = _.extend({}, this.events, ...new events...);
instead of
_.extend(this.events, ...new events...);
Doing it the second way will munge the events object within the parent's (ElementView) prototype. The first way ensures each model gets its own copy.

Related

Backbone view optimization

I'm using backbone js in my Project, I'm struck in a small confusion with views.
I'm having these 2 views. After writing them i am in a confusion whether i'm in right path or not. The reason for my doubt is that the code was looking almost the same except that the el in which the view is rendered and the template that is used in the view.
Will this type of code effect the performance?? can I optimize it ?
code:
Project.views.list = Backbone.View.extend({
// The DOM Element associated with this view
el: '.lists-section-content',
itemView: function(x){
return new Project.views.list(x);
},
// View constructor
initialize: function(payload) {
this.data = payload.data;
this.colStr = payload.colStr;
this.render();
},
events: {
},
render: function() {
sb.renderXTemplate(this);
return this;
}
});
Firstly you be better to provide el value at first element of tree initialization, otherwise all views will try to use same DOM element(s):
var myTreeRoot= new Project.views.list({
el: '.lists-section-content',
data: payload.data,
colStr: payload.colStr
});
After this you'll need to modify initialize function a little to utilize options argument of view constructor:
// View constructor
initialize: function(options) {
this.data = options.data;
this.colStr = options.colStr;
this.render();
},
And finally answering to your question, no this way it will not affect performance. You just need to track leaf views inside parent view and remove them with parent, it's needed to avoid memory leaks. Here is example of cleanup (all leaf views collected with this.subViews array and removed on parent removal):
Project.views.list = Backbone.View.extend({
// The DOM Element associated with this view
itemView: function(x){
var view = new Project.views.list(x)
this.subViews.push(view)
this.$('.item-container:first').append(view.el)
},
remove: function() {
_.invoke(this.subViews, 'remove')
return Backbone.View.prototype.remove.apply(this, arguments)
},
// View constructor
initialize: function(options) {
this.data = options.data;
this.subViews = [];
this.colStr = options.colStr;
this.render();
},
render: function() {
sb.renderXTemplate(this);
return this;
}
});

How to call a function of a parent class from another function in jquery

I guess that's the simple question. I'm new in js, especially in Backbone.js.
All I want to know is how I can refer to my function inside jquery function.
getLanguages: function() {
...
return languages;
},
render: function() {
...
$("input[type='checkbox']").bind("change", function() {
// todo: getLanguages
});
}
I tried to get languages via this but, of course, I got checkbox in this case.
Edit:
It's so simple. Many thanks to all!!!
This is a classic problem in Javascript. You need to reference this inside a callback, but this changes to the element being bound to. A cheap way to do it:
render: function() {
var that = this;
$("input[type='checkbox']").bind("change", function() {
that.getLanguages();
});
}
that will stay defined as the this that render is defined on.
However, you’re using Backbone, and it has more suitable ways to handle this situation. I don’t know the name of your Backbone.View class, but here’s an example adapted from the documentation:
var DocumentView = Backbone.View.extend({
events: {
"change input[type='checkbox']": "doSomething"
},
doSomething: function() {
this.getLanguages(); # uses the correct this
}
});
Calling bind inside render is not The Backbone Way. Backbone views are made to handle event delegation without the unfortunate need to pass this around.
Save this object before bind change event in the scope of render function.
render: function() {
var CurrentObj = this;
$("input[type='checkbox']").bind("change", function() {
CurrentObj.getLanguages();
});
}
You can save the appropriate object into a local variable so from the event handler, you can still get to it:
getLanguages: function() {
...
return languages;
},
render: function() {
...
var self = this;
$("input[type='checkbox']").bind("change", function() {
var lang = self.getLanguages();
...
});
}

Testing Backbone Events set up in the initialize function

I'm trying to set up my collection to listen to events triggered on models within the collection, like so:
var Collection = Backbone.Collection.extend({
initialize: function() {
this.on('playback:completed', this.playNext);
},
playNext : function() { }
});
In my tests, I add new Backbone.Models into an instance of the collection, and then trigger playback:completed on them... and playNext isn't called. How do I set this up correctly?
EDIT: adding test code (uses Jasmine):
var collection;
describe('Collection', function() {
beforeEach(function() {
collection = new Collection();
});
it('should playNext when playback:completed is triggered', function() {
var model1 = new Backbone.Model();
var model2 = new Backbone.Model();
var spy = spyOn(collection, 'playNext').andCallThrough();
collection.add(model1);
collection.add(model2);
model1.trigger('playback:completed');
expect(spy).toHaveBeenCalled();
});
});
The problem here is that backbone's .on wraps the callback as a clone or something like it. Since the spy is set up after Collection.initialize runs, the callback that's been wrapped is the function before setting the spy, it's never triggered.
The solution I settled on was to pull the event bindings into a bindEvents function, like so:
var Collection = Backbone.Collection.extend({
initialize: function() {
this.bindEvents();
},
bindEvents : function() {
this.on('playback:completed', this.playNext);
},
playNext : function() { }
});
Then, in my test (and after the spy is set), I ran collection.off(); collection.bindEvents();, which re-binds them with the spied versions.

Strange issue binding events with backbone, "this" is not being updated

I had a strange issue working with backbone and binding events. I'll see if I can explain it in a clear way (it's a cropped example...)
In a view, I had the following code in the initialize method
var MyView = Backbone.View.extend({
initialize: function(options) {
//[...]
this.items = [];
this.collection.on('reset', this.updateItems, this);
this.fetched = false;
},
render: function() {
if (!this.fetched) {
this.collection.fetch(); // fetch the collection and fire updateItems
return this;
}
this.$el = $('#my-element');
this.$el.html(this.template(this.items));
},
updateItems: function() {
this.fetched = true;
this.loadItems();
this.render(); // call render with the items array ready to be displayed
}
}
The idea is that I have to fetch the collection, process the items (this.loadItems), and then I set this.$el.
The problem I was facing, is that inside updateItems, I couldn't see any property added after the binding (this.collection.on...)
It seemed like the binding was done against a frozen version of the view. I tried adding properties to test it, but inside updateItems (and inside render if being fired by the collection reset event) I could not see the added properties.
I solved it binding the collection just before fetching it, like this:
render: function() {
if (!this.fetched) {
this.collection.on('reset', this.updateItems, this);
this.collection.fetch();
return this;
}
But it's a strange behavior. Seems like when binding, a copy of 'this' is made, instead of a reference.
Am I right? or there's anything wrong I'm doing?
You should perform your binding in the initialization phase of your collection view:
// View of collection
initialize: function() {
this.model.bind('reset', this.updateItems);
}
now when fetch is finished on the collection updateItems method will be invoked.
Of course you need to bind the model and view before doing this:
var list = new ListModel();
var listView = new ListView({model: list});
list.fetch();

Backbone.js Persisting child variables to parent

What is the correct way to persist an inherited variable, on action to the parent in Backbone.js?
I can see some logical ways to do this but they seem inefficient and thought it might be worth asking for another opinion.
The two classes are both views which construct a new model to be saved to a collection, the parent passing a variable through to a popup window where this variable can be set.
I'm not sure there's enough detail in your question to answer, but there are a few ways to to do this:
Share a common model. As you describe it, you're using two views to construct a model, so the easiest way is probably to pass the model itself to the child view and have the child view modify the model, rather than passing any variables between views:
var MyModel = Backbone.Model.extend({});
var ParentView = Backbone.View.extend({
// initialize the new model
initialize: function() {
this.model = new MyModel();
},
// open the pop-up on click
events: {
'click #open_popup': 'openPopUp'
},
openPopUp: function() {
// pass the model
new PopUpView({ model: this.model })
}
});
var PopUpView = Backbone.View.extend({
events: {
'change input#someProperty': 'changeProperty'
},
changeProperty: function() {
var value = $('input#someProperty').val();
this.model.set({ someProperty : value });
}
});
Trigger an event on the parent. If for some reason you can't just pass the value via the model, you can just pass a reference to the parent and trigger an event:
var ParentView = Backbone.View.extend({
initialize: function() {
// bind callback to event
this.on('updateProperty', this.updateProperty, this);
},
updateProperty: function(value) {
// do whatever you need to do with the value here
},
// open the pop-up on click
events: {
'click #open_popup': 'openPopUp'
},
openPopUp: function() {
// pass the model
new PopUpView({ parent: this })
}
});
var PopUpView = Backbone.View.extend({
events: {
'change input#someProperty': 'changeProperty'
},
changeProperty: function() {
var value = $('input#someProperty').val();
this.options.parent.trigger('updateProperty', value);
}
});

Categories