What is more efficient, using events to communicate between nested Views, or keeping a reference around to call a method on. The following example shows two Views. The outer View responds to a click event, and could then use either an event, or method call to get the InnerView to respond appropriately.
InnerView = Backbone.View.extend({
initialize: function() {
this.model.bind('doSomethingEvent', this.doSomething);
},
doSomething: function() {
// This could have been called from event/trigger
// or from direction method invocation using reference.
}
});
OuterView = Backbone.View.extend({
events = {
'click' : 'handleOutViewClick'
},
render: function() {
// Create InnerView to render some model
var innerView = new InnerView({model:this.model });
$(this.el).append(innerView.render().el);
// Could store a reference to the View?
this.viewRef = innerView;
},
handleOutViewClick: function(e) {
// Should this function use a reference to the InnerView instance:
this.viewRef.doSomething();
// Or should it trigger an event on this.model that
// the InnerView is bound to?
this.someCollection.trigger('doSomethingEvent');
}
});
Probably a single method call is going to be more efficient than an event dispatch, which will involve at least 2 method calls. But I don't think you need to be concerned about which is more "efficient" here technically. Unless this is happening many times a second, you can concern yourself only with what makes for cleaner, more correct code. I think the cleanest pattern depends on the details being communicated. Here's my preference:
If it is a natural fit for outerview to manipulate models and collections, and have innerview respond via the normal backbone model/collection events, that is the cleanest.
If what's happening isn't really about the models, consider a "View Model" pattern where you model the interesting bits of state of the view as if it was a back end model, even though you have no intention of ever having that model interact with the server. Then bind your two views to events coming off the view model and have them coordinate by altering a common "view model" instance. This is a pattern I use for complicated views with lots of interdependent state not directly associated with the underlying models from the server.
If what's happening doesn't really change the models/collections and is more of a view-specific thing, a direct method dispatch will be more straightforward but also more tightly coupled. It's up to your judgement to decide when the loose coupling afforded by event dispatch merits the extra complexity and harder-to-follow control flow.
Related
I am building an app using backbone.js and find myself with a view with a lot of conditional logic in the templating of the Model's View. The type property of the model is used to determine which html to render. I would like to avoid this logic if possible, because it is hard to read. There are a couple of ways that I think I could deal with this (Note that the actual templates I've got here are very much simplified):
1. Conditional Logic in Collection View rather than in Model View - Multiple Subviews
I could put the conditional logic that acts on each model's type into the Collection View:
var CollectionView = Backbone.View.extend({
....
render: function() {
this.collection.each(function(thing) {
if(thing.get("type") === "wotsit") {
this.$el.append(new WotsitView({ model: thing });
} else if(thing.get("type") === "oojamaflip") {
this.$el.append(new OojamaflipView({ model: thing });
}
}, this);
},
....
}
Pros
This way I could have each subview with a template method that had no logic in it, but rather just builds html.
var WotsitView = new Backbone.View.extend({
....
template: _.template('<h2>{{ title }}</h2>');
});
var OojamaflipView = new Backbone.View.extend({
....
template: _.template('<h3>{{ title }}</h3>');
});
Cons
The thing is, the things in the collection are all very similar. The events for each thing are likely to be the same or very similar and I can see there being a lot of code duplication. I really only want the actual template for these subviews to be different with everything else the same.
2. Conditional Logic in Model View - Multiple Template Methods
var ModelView = Backbone.View.extend({
....
render: function() {
if(this.model.get("type") ==== "wotsit") {
this.$el.html(this.wotsitTemplate(this.model.attributes));
} else if(this.model.get("type") === "oojamaflip") {
this.$el.html(this.oojamaflipTemplate(this.model.attributes));
}
},
wotsitTemplate: _.template('<h2>{{ title }}</h2>'),
oojamaflipTemplate: _.template('<h3>{{ title }}</h3>')
});
Pros
There is only one view for the model.
All of the events etc are handles in one view rather that being duplicated.
Cons
I actually quite like this way, but I would be very interested to hear some other peoples options on it.
Option #1 = polymorphism
Option #2 = switch statement
Switch statements are useful if you never (or rarely) have to add new types, but you might want to add new methods. Imagine you want to add a validate method to ModelView, which checks view input for that kind of model for correctness and reports the results to the user. With option #2, you'd just add one new method that switches on the model type (just like the render method) to handle validation.
Now let's assume we already have a render method and a validate method, and we want to handle a new type of model, a thingamajig. With option #2, you'd have to add logic to both render and validate. Now imagine we don't have just 2 methods, but 10 — option #2 gets real complicated real quick when you have to handle new types. But if we followed option #1, then no matter how many methods there were, we'd only have to create a new view in one place, and update CollectionView to map the new type to the new view.
Most of the time polymorphism (option #1) is the clean way to go. It lets you separate all logic specific to a given type and put it one place, making it easy to handle new types.
Keeping DRY
If you're worried with option #1 that you'll end up with a lot of duplicate code between all the views, don't forget that it's easy to make Views that inherit from other Backbone Views:
var ItemView = Backbone.View.extend({
initialize: function() {
/* some common logic for all views */
}
});
var WotsitView = ItemView.extend({
template: _.template('<h2>{{ title }}</h2>')
});
var OojamaflipView = ItemView.extend({
template: _.template('<h3>{{ title }}</h3>')
});
Usually I have a View on which I set listeners to model-changes, like so (conflated):
var jsonModel = {bla: 'interesting stuff'}; //some model in json, probably rendered in a dom-element and passed from the server to the client
var someModelType = Backbone.RelationalModel.extend({
bla: "String"
});
var someModelInstance = new someModelType(jsonModel);
var someViewType = Backbone.View.extend({
initialize: function(){
this.listenTo(this.model,'change:bla', function(model){
//update view to sync with model change here
}
}
});
var someViewInstance = new someViewType({
model: someModelInstance
});
I'm looking for the preferred / backbone - way of bootstrapping the view, i.e: I want my view (form fields or what have you) to be synced with the model on creation. With the above this is impossible since the model is created before the view is attached, which results in model-updates/changes to be fired before listeners from the view were initialized.
Of course I could code some custom bootstapping logic which would call the listener-functions manually, but since this must be such a common problem, I'm looking on some best practice advice, or even better, a Backbone-switch I need to set to get this to work.
This will sound silly, but what I usually do is do a render() after the view creation.
For me, the whole point of the change event is to notify of a change. The fact that it's triggered also on creation (as a "changed compared to the default") is more a side effect than anything deep...
In that sense, when I create the views, and give it an existing model, I expect the model to be somewhat ready and so I should be able to do a render() right away. So my bootstrapping code would be more like:
var someViewInstance = new someViewType({
model: someModelInstance
});
someViewInstance.render()
Or do it in the initialize() after bindings if you feel adventurous (I personally don't like that because when dealing with subviews, it's hard to know exactly when you want the render to happen, especially when dependent on libraries who need the DOM to be ready, and can't work on detached nodes).
So to sum up, although your problem is common, to me, it doesn't need any kind of complex solution: you have a render() function to transform your model into DOM-stuff, your model may or may not be ready but it exists, so you pass it to the view, and you do the first rendering after creating the view, manually, because you know it needs to happen. Then you bind to changes, in case your model is updated, to update the DOM accordingly.
side note: in case of a collection of models, when the Model isn't ready at ALL, then it probably shouldn't even have an instance, and you should let the Collection give you some add/remove events for when it pops into existence.
I am trying to test drive a view event using Jasmine and the problem is probably best explained via code.
The view looks like:
App.testView = Backbone.View.extend({
events: { 'click .overlay': 'myEvent' },
myEvent: function(e) {
console.log('hello world')
}
The test looks something like:
describe('myEvent', function() {
it('should do something', function() {
var view = new App.testView();
view.myEvent();
// assertion will follow
});
});
The problem is that the view.myEvent method is never called (nothing logs to the console). I was trying to avoid triggering from the DOM. Has anyone had similar problems?
(Like I commented in the question, your code looks fine and should work. Your problem is not in the code you posted. If you can expand your code samples and give more info, we can take another look at it. What follows is more general advice on testing Backbone views.)
Calling the event handler function like you do is a legitimate testing strategy, but it has a couple of shortcomings.
It doesn't test that the events are wired up correctly. What you're testing is that the callback does what it's supposed to, but it doesn't test that the action is actually triggered when your user interacts with the page.
If your event handler needs to reference the event argument or the test will not work.
I prefer to test my views all the way from the event:
var view = new View().render();
view.$('.overlay').click();
expect(...).toEqual(...);
Like you said, it's generally not advisable to manipulate DOM in your tests, so this way of testing views requires that view.render does not attach anything to the DOM.
The best way to achieve this is leave the DOM manipulation to the code that's responsible for initializing the view. If you don't set an el property to the view (either in the View.extend definition or in the view constructor), Backbone will create a new, detached DOM node as view.el. This element works just like an attached node - you can manipulate its contents and trigger events on it.
So instead of...
View.extend({el: '#container'});
...or...
new View({el:'#container'});
...you should initialize your views as follows:
var view = new View();
$("#container").html(view.render().el);
Defining your views like this has multiple benefits:
Enables testing views fully without attaching them to DOM.
The views become reusable, you can create multiple instances and render them to different elements.
If your render method does some complicated DOM manipulation, it's faster to perform it on an detached node.
From a responsibility point of view you could argue that a view shouldn't know where it's placed, in the same way a model should not know what collection it should be added to. This enforces better design of view composition.
IMHO, this view rendering pattern is a general best practice, not just a testing-related special case.
Suppose you are making a music library app.
You have one view with a list on genres and another that shows the contents of the selected genre. When the user clicks a genre on the list, the contents in the other view should be updated accordingly.
What is the best way to do this so that there are minimal dependencies?
I have not found any other place to listen to the mouse clicks than the view that draws the individual genre. I can send an event from there, but what is the optimal way of getting that event to update the other view which draws the genre contents? Who should listen to that event, the genre content view or its collection?
EDIT: I instantiate both views from the router of the app and I did manage to get this to work by making the views aware of each other, but that's not optimal of course.
You could make a simple model to hold the application state, you don't need anything fancy, just a bag of data that implements the usual Backbone event methods:
var AppState = Backbone.Model.extend({});
var app_state = new AppState();
Then the genre list view would listen for click events (as you already have) and set the current genre on the app-state model when someone changes it:
var Genres = Backbone.View.extend({
//...
choose: function(ev) {
// This would be the click handler for the genre,
// `.html()` is just for demonstration purposes, you'd
// probably use a data attribute in real life.
app_state.set({genre: $(ev.target).html() });
},
});
The view for the individual genre would listen for "change:genre" events on the app-state model and react as the genre changes:
var Genre = Backbone.View.extend({
initialize: function() {
_.bindAll(this, 'change_genre');
app_state.on('change:genre', this.change_genre);
},
//...
change_genre: function() {
// Refill the genre display...
}
});
Demo: http://jsfiddle.net/ambiguous/mwBKm/1/
You can make models for any data you want and models are a convenient way of working with data events in Backbone. As an added bonus, this approach makes it fairly easy to persist your application's state: just add the usual Backbone persistence support to AppState and away you go.
If you only need a simple event bus to push non-data events around, you can use Backbone's Events methods to build a simple event aggregator:
app.events = _.extend({}, Backbone.Events);
Then, assuming you have a global app namespace, you can say things like this:
app.events.on('some-event', some_function);
and
app.events.trigger('some-event', arg1, arg2, ...);
The best way I have seen is in the method proposed by Derick Bailey. In short you can create an event aggregator, which provides a centralized object for raising events to which different views can subscribe. The beauty of this solution is that it is very simple to implement as it makes use of Backbone's existing event system.
What is the best way to bind events to a Backbone boilerplate application? I've been trying to bind my events directly to the models associated with my views, in my views, but it doesn't seem to be working. I see within 'namespace.js', that there is an app key that extends Backbone.Events like so:
// Keep active application instances namespaced under an app object.
app: _.extend({}, Backbone.Events)
I don't fully understand how to use it...
I was able to get things working without the boilerplate, but it does provide some very cool functionality, so I'd love to be able to use it. Thanks!
ADDED
the code I was using was with the underscore bind method like so:
this.module.bind('change', this.render);
But then, I realized that 'this.model' is returning undefined, and so this doesn't work. I really am not sure how the boilerplate wants me to reference my model from the view.
I'm not sure if it is a typo that you copied from your code or a typo you only entered here, but I believe this.module (which IS undefined) should be this.model, which you also must be sure to pass in when you instantiate your view, of course, as so:
myView = new BBView({model: myModel});
then you can say this.model.bind('change', this.render); or this.model.on('change', this.render); for the most recent version of Backbone
I frequently bind my views to change events on my models in this way.
As for extending Backbone.Events, the way I have used it is to create an independent "event aggregator" that will help connect events between different views on your page. Let's say for example you have some action on one view that needs to set off an action on another view. In this case, you can pass your event aggregator object as an option to each of your views when you instantiate them, so that they can both trigger events or bind to events on a common object (i.e. your event aggregator).
whatsUp = _.extend({}, Backbone.Events) // the aggregator
myFirstView = new FirstBBView ({whatsUp: whatsUp});
(the aggregator shows up as this.options.whatsUp inside the view)
mySecondView = new SecondBBView2 ({whatsUp: whatsUp});
inside FirstBBView:
this.options.whatsUp.bind('specialEvent', function(arg1,arg2) {
// do stuff
});
inside SecondBBView, when something important happens:
this.options.whatsUp.trigger('specialEvent', {arg1: 'some data', arg2: 'more data'});
For a great explanation, see this great article by Derick Bailey