I'm wondering how best to handle views and subviews using a Javascript OOP approach. Here's the basic task: I want to create a horizontal, swipe-able list of panes. The swipe element itself will be a view, and within it I will have many subviews -- one for each pane. My current approach looks something like this:
function Swipe(el){
this.el = el;
var els = el.querySelectorAll('li');
this.subviews = els.map(function(li){
return new Pane(li);
}, this);
}
Swipe.prototype.goto = function(num){
// alter this.el to show the current pane, based
// on index.
}
function Pane(el){
this.el = el;
}
Pane.prototype.bind = function(){
// bind click events
}
This design pattern works decently well for me, but what is the best way to access the parent view from the subview? I've gone so far as to pass this when constructing the subview, like this:
this.subviews = els.map(function(li){
return new Pane(li, this);
}, this);
function Pane(el, parent){
this.el = el;
this.parent = parent;
}
And this allows me to call functions from the parent view -- for example, I could use the goto function in response to a subview button being clicked. But I'm guessing that this isn't really optimal.
So my question: what is the best way to handle views and subviews, specifically the ability of subviews to call functions from their parent views?
is it possible to pass the questions variable into the view render?
Ive attempted calling this.render inside the success on the fetch however I got an error, presumably it's because this. is not at the correct scope.
app.AppView = Backbone.View.extend({
initialize: function(){
var inputs = new app.Form();
inputs.fetch({
success: function() {
var questions = inputs.get(0).toJSON().Questions;
app.validate = new Validate(questions);
app.validate.questions();
}, // End Success()
error: function(err){
console.log("Couldn't GET the service " + err);
}
}); // End Input.fetch()
this.render();
}, // End Initialize
render: function(){
el: $('#finder')
var template = _.template( $("#form_template").html(), {} );
this.$el.html(template);
}
The success callback is called with a different this object than your View instance.
The easiest way to fix it is to add something like this before you call inputs.fetch:
var self = this;
And then inside the success callback:
self.render();
I'm not quite sure what you're trying to achieve, but if your problem is calling render from the success callback, you have two options, Function#bind or assigning a self variable.
For more information about "self" variable, see var self = this? . An example:
var self = this;
inputs.fetch({
success: function () {
self.render();
},
...
});
You should probably do some reading on JavaScript scopes, for example "Effective Javascript" or search the topic ( for example this MDN article ) online to get a better idea what happens there.
For Function#bind(), see the MDN article about it. With Backbone I suggest you use Underscore/LoDash's _.bind instead though, to make sure it works even where Function#bind() is not supported.
As for more high-level concepts, the fetching operation looks like it belongs to the model or router level and instead you should assign the questions variable as the model of your view. Ideally views don't do data processing / fetching, they're just given a model that has the methods necessary to perform any data transformations you might need.
The views shouldn't even need to worry about where the model comes from, this is normally handled by a router in case of single page applications or some initialization code on the page.
I'm working on a toy backbone.js application, a library application to perform CRUD operations on a library. Here is the book model and the library collection (of books)
var Book = Backbone.Model.extend({
url: function() {
return '/api/books' + this.get('id');
}
});
var Library = Backbone.Collection.extend({
model : Book,
url : '/api/books'
});
This seems pretty straightforward. Next, I obviously want to be able to show the book, so I have a BookView...
var BookView = Backbone.View.extend({
tagName: 'li',
render: function() {
this.$el.html(this.model.get('author'));
$('#list-of-books').append(this.$el);
}
});
all the render method does is append an li to the end of an unordered list with an id of list-of-books that is in my html.
Next, unsurprisingly, if I add the following code, I get a list with one item (the name of the author of the book with id=4)
var a_book = new Book();
a_book.url = '/api/books/4';
a_book.fetch({
success: function() {
var bookView = new BookView({ model: a_book });
bookView.render();
}
});
Here's where I don't understand. I add the following code and nothing happens:
var some_books = new Library();
some_books.fetch();
some_books.forEach(function(book) {
alert('why is this function not being run');
var view = new BookView({ model: book });
view.render();
});
For some reason, I can't even get that loop code to run, I don't even see that alert pop out to the screen. Let me know if you understand what's wrong with this code and how I can make the list render properly. Thanks
You are calling .render inside fetch. Hence the execution stops over there itself. For loop wont run after fetch as it has returned already.
.fetch() is an asynchronous call, so you may not have the collection data populated when you are calling .forEach().
You should either call the rendering code in the success callback or (even better) render the view on events (reset,...) on the collection.
BTW, you have a typo in the url() function:
url: function() {
return '/api/books/' + this.get('id');
}
Why are you defining the url function for the Book?, since you use the model attribute in the Library collection, url for the Book will be set correctly by default.
Is there a standard way to deal with non-saveable values in Backbone.
e.g.
MyModel = Backbone.extend(Backbone.Model, {
initialize: function () {
this.set({'inches': this.get('mm') / 25});
}
})
If I call save() on this model it will throw an error as there is no corresponding database field for inches. I can think of a few ways to fix this, but am wondering if there's a tried and tested approach generally best used for this?
At the moment my preferred solution is to extend Backbone's toJSON method and to allow passing of a boolean parameter dontCleanup to allow for it to still return all the model's values (including the non saveable ones) when it's needed e.g. for passing to a template.
I like Peter Lyon's idea. I've thought about that a few times, but never actually put it in place. For all the ways that I have handled this, though, here are my two favorites:
Non-"attribute" values
View Models
Non-Attribute Values
This one is simple: don't store the values you need in the model's standard attributes. Instead, attach it directly to the object:
myModel.someValue = "some value";
The big problem here is that you don't get all of the events associated with calling set on the model. So I tend to wrap this up in a method that does everything for me. For example, a common method I put on models is select to say that this model has been selected:
MyModel = Backbone.Model.extend({
select: function(){
if (!this.selected){
this.selected = true;
this.trigger("change:selected", this, this.selected);
}
}
});
In your case, I'm not sure this would be a good approach. You have data that needs to be calculated based on the values that are in your attributes already.
For that, I tend to use view models.
View models.
The basic idea is that you create a backbone model that is persist-able, as you normally would. But the you come along and create another model that inherits from your original one and adds all the data that you need.
There are a very large number of ways that you can do this. Here's what might be a very simple version:
MyModel = Backbone.Model.Extend({ ... });
MyViewModel = function(model){
var viewModel = Object.create(model);
viewModel.toJSON = function(){
var json = model.toJSON();
json.inches = json.mm / 25;
return json;
};
return viewModel;
});
The big benefit of wrapping this with Object.create is that you now have a prototypal inheritance situation, so all of your standard functionality from the model is still in place. We've just overridden the toJSON method on the view model, so that it returns the JSON object with the inches attribute.
Then in a view that needs this, you would wrap your model in the initialize function:
MyView = Backbone.View.extend({
initialize: function(){
this.model = MyViewModel(this.model);
},
render: function(){
var data = this.model.toJSON(); // returns with inches
}
});
You could call new MyViewModel(this.model) if you want, but that's not going to do anything different, in the end, because we're explicitly returning an object instance from the MyViewModel function.
When your view's render method calls toJSON, you'll get the inches attribute with it.
Of course, there are some potential memory concerns and performance concerns with this implementation, but those can be solved easily with some better code for the view model. This quick and dirty example should get you down the path, though.
I think this should do it. Define your Model defaults as your valid schema and then return only the subset of this.attributes that is valid during toJSON.
var Model = Backbone.Model.extend({
defaults: {
foo: 42,
bar: "bar"
},
toJSON: function () {
var schemaKeys = _.keys(this.defaults);
var allowedAttributes = {};
_.each(this.attributes, function (value, key) {
if (_.include(schemaKeys, key)) {
allowedAttributes[key] = value;
}
return allowedAttributes;
}
});
Note that _.pick would make the code a bit shorter once you have underscore 1.3.3 available. I haven't seen a "tried and tested" convention in my travels through the backbone community, and since backbone leaves so many options open, sometimes conventions don't emerge, but we'll see what this stackoverflow question yields.
Dealing with non-persisted attributes in Backbone.js has been doing my head in for a while, particularly since I've started using ember/ember-data, which handles the various situations through computed properties, ember-data attributes, or controllers.
Many solutions suggest customising the toJSON method. However, some popular Backbone plugins (particularly those that deal with nested models), implement their own toJSON method, and make a call to Backbone.Model.prototype.toJSON to obtain an object representation of a model's attributes. So by overwriting the toJSON method in a model definition, you'll lose some (potentially crucial) features of those plugins.
The best I've come up with is to include an excludeFromJSON array of keys in the model definition, and overwrite the toJSON method on Backbone.Model.prototype itself:
Backbone.Model.prototype.toJSON = function() {
var json = _.clone(this.attributes),
excludeFromJSON = this.excludeFromJSON;
if(excludeFromJSON) {
_.each(excludeFromJSON, function(key) {
delete json[key];
});
}
return json;
};
MyModel = Backbone.Model.extend({
excludeFromJSON: [
'inches'
]
});
In this way, you'll only have to define the non-persisted keys (if you forget to do so, you'll soon be reminded when your server throws an error!). toJSON will behave as normal if no excludeFromJSON property is present.
In your case, inches is a computed property, derived from mm, so it makes sense to implement this as a method on your model (ensuring that the value for inches is correct when mm is changed):
MyModel = Backbone.Model.extend({
inches: function() {
return this.get('mm') / 25;
}
});
However, this has the downside of being accessed differently to everyother attribute. Ideally you'll want to keep it consistent with accessing other attributes. This can be achieved by extending the default get method:
var getMixin = {
get: function(attr) {
if(typeof this[attr] == 'function') {
return this[attr]();
}
return Backbone.Model.prototype.get.call(this, attr);
}
};
MyModel = Backbone.Model.extend({
inches: function() {
return this.get('mm') / 25;
}
});
_.extend(MyModel.prototype, getMixin);
Which will let you do:
new MyModel().get('inches');
This approach doesn't touch the underlying attributes hash, meaning that inches will not appear in the toJSON representation, unless you set the value of inches later on, in which case you'll need something like the excludeFromJSON array.
If you have the need to set the inches value, you may also want to listen for changes and adjust the value of mm:
MyModel = Backbone.Model.extend({
initialize: function() {
this.on('change:inches', this.changeInches, this);
},
inches: function() {
return this.get('mm') / 25;
},
changeInches: function() {
this.set('mm', this.attributes.inches * 25);
}
});
_.extend(MyModel.prototype, getMixin);
See the complete example on JSBin.
It's also worth noting that the (official?) purpose of the toJSON method has recently been redefined as preparing a model for syncing with a server. For this reason, calling toJSON should always return only the "persistable" (or "saveable") attributes.
I have a nested-View setup which can get somewhat deep in my application. There are a bunch of ways I could think of initializing, rendering and appending the sub-views, but I'm wondering what common practice is.
Here are a couple I've thought of:
initialize : function () {
this.subView1 = new Subview({options});
this.subView2 = new Subview({options});
},
render : function () {
this.$el.html(this.template());
this.subView1.setElement('.some-el').render();
this.subView2.setElement('.some-el').render();
}
Pros: You don't have to worry about maintaining the right DOM order with appending. The views are initialized early on, so there isn't as much to do all at once in the render function.
Cons: You are forced to re-delegateEvents(), which might be costly? The parent view's render function is cluttered with all of the subview rendering that needs to happen? You don't have the ability to set the tagName of the elements, so the template needs to maintain the correct tagNames.
Another way:
initialize : function () {
},
render : function () {
this.$el.empty();
this.subView1 = new Subview({options});
this.subView2 = new Subview({options});
this.$el.append(this.subView1.render().el, this.subView2.render().el);
}
Pros: You don't have to re-delegate events. You don't need a template that just contains empty placeholders and your tagName's are back to being defined by the view.
Cons: You now have to make sure to append things in the right order. The parent view's render is still cluttered by the subview rendering.
With an onRender event:
initialize : function () {
this.on('render', this.onRender);
this.subView1 = new Subview({options});
this.subView2 = new Subview({options});
},
render : function () {
this.$el.html(this.template);
//other stuff
return this.trigger('render');
},
onRender : function () {
this.subView1.setElement('.some-el').render();
this.subView2.setElement('.some-el').render();
}
Pros: The subview logic is now separated from the view's render() method.
With an onRender event:
initialize : function () {
this.on('render', this.onRender);
},
render : function () {
this.$el.html(this.template);
//other stuff
return this.trigger('render');
},
onRender : function () {
this.subView1 = new Subview();
this.subView2 = new Subview();
this.subView1.setElement('.some-el').render();
this.subView2.setElement('.some-el').render();
}
I've kind of mix and matched a bunch of different practices across all of these examples (so sorry about that) but what are the ones that you would keep or add? and what would you not do?
Summary of practices:
Instantiate subviews in initialize or in render?
Perform all sub-view rendering logic in render or in onRender?
Use setElement or append/appendTo?
I have generally seen/used a couple of different solutions:
Solution 1
var OuterView = Backbone.View.extend({
initialize: function() {
this.inner = new InnerView();
},
render: function() {
this.$el.html(template); // or this.$el.empty() if you have no template
this.$el.append(this.inner.$el);
this.inner.render();
}
});
var InnerView = Backbone.View.extend({
render: function() {
this.$el.html(template);
this.delegateEvents();
}
});
This is similar to your first example, with a few changes:
The order in which you append the sub elements matters
The outer view does not contain the html elements to be set on the inner view(s) (meaning you can still specify tagName in the inner view)
render() is called AFTER the inner view's element has been placed into the DOM, which is helpful if your inner view's render() method is placing/sizing itself on the page based on other elements' position/size (which is a common use case, in my experience)
Solution 2
var OuterView = Backbone.View.extend({
initialize: function() {
this.render();
},
render: function() {
this.$el.html(template); // or this.$el.empty() if you have no template
this.inner = new InnerView();
this.$el.append(this.inner.$el);
}
});
var InnerView = Backbone.View.extend({
initialize: function() {
this.render();
},
render: function() {
this.$el.html(template);
}
});
Solution 2 may look cleaner, but it has caused some strange things in my experience and has affected performance negatively.
I generally use Solution 1, for a couple of reasons:
A lot of my views rely on already being in the DOM in their render() method
When the outer view is re-rendered, views don't have to be re-initialized, which re-initialization can cause memory leaks and also cause freaky issues with existing bindings
Keep in mind that if you are initializing a new View() every time render() is called, that initialization is going to call delegateEvents() anyway. So that shouldn't necessarily be a "con", as you've expressed.
This is a perennial problem with Backbone and, in my experience, there's not really a satisfying answer to this question. I share your frustration, especially since there is so little guidance despite how common this use case is. That said, I usually go with something akin to your second example.
First of all, I would dismiss out of hand anything that requires you to re-delegate events. Backbone's event-driven view model is one of its most crucial components, and to lose that functionality simply because your application is non-trivial would leave a bad taste in any programmer's mouth. So scratch number one.
Regarding your third example, I think it's just an end-run around the conventional rendering practice and doesn't add much meaning. Perhaps if you're doing actual event triggering (i.e., not a contrived "onRender" event), it would be worth just binding those events to render itself. If you find render becoming unwieldy and complex, you have too few subviews.
Back to your second example, which is probably the lesser of the three evils. Here is example code lifted from Recipes With Backbone, found on page 42 of my PDF edition:
...
render: function() {
$(this.el).html(this.template());
this.addAll();
return this;
},
addAll: function() {
this.collection.each(this.addOne);
},
addOne: function(model) {
view = new Views.Appointment({model: model});
view.render();
$(this.el).append(view.el);
model.bind('remove', view.remove);
}
This is only a slightly more sophisticated setup than your second example: they specifiy a set of functions, addAll and addOne, that do the dirty work. I think this approach is workable (and I certainly use it); but it still leaves a bizarre aftertaste. (Pardon all these tongue metaphors.)
To your point on appending in the right order: if you're strictly appending, sure, that's a limitation. But make sure you consider all possible templating schemes. Perhaps you'd actually like a placeholder element (e.g., an empty div or ul) that you can then replaceWith a new (DOM) element that holds the appropriate subviews. Appending isn't the only solution, and you can certainly get around the ordering problem if you care about it that much, but I would imagine you have a design issue if it is tripping you up. Remember, subviews can have subviews, and they should if it's appropriate. That way, you have a rather tree-like structure, which is quite nice: each subview adds all its subviews, in order, before the parent view adds another, and so on.
Unfortunately, solution #2 is probably the best you can hope for using out-of-the-box Backbone. If you're interested in checking out third-party libraries, one that I have looked into (but haven't actually had any time to play with yet) is Backbone.LayoutManager, which seems to have a healthier method of adding subviews. However, even they have had recent debates on similar issues to these.
Surprised this hasn't been mentioned yet, but I'd seriously consider using Marionette.
It enforces a bit more structure to Backbone apps, including specific view types (ListView, ItemView, Region and Layout), adding proper Controllers and a lot more.
Here is the project on Github and a great guide by Addy Osmani in the book Backbone Fundamentals to get you started.
I have, what I believe to be, a quite comprehensive solution to this problem. It allows a model within a collection to change, and have only its view re-rendered (rather than the entire collection). It also handles removal of zombie views through the close() methods.
var SubView = Backbone.View.extend({
// tagName: must be implemented
// className: must be implemented
// template: must be implemented
initialize: function() {
this.model.on("change", this.render, this);
this.model.on("close", this.close, this);
},
render: function(options) {
console.log("rendering subview for",this.model.get("name"));
var defaultOptions = {};
options = typeof options === "object" ? $.extend(true, defaultOptions, options) : defaultOptions;
this.$el.html(this.template({model: this.model.toJSON(), options: options})).fadeIn("fast");
return this;
},
close: function() {
console.log("closing subview for",this.model.get("name"));
this.model.off("change", this.render, this);
this.model.off("close", this.close, this);
this.remove();
}
});
var ViewCollection = Backbone.View.extend({
// el: must be implemented
// subViewClass: must be implemented
initialize: function() {
var self = this;
self.collection.on("add", self.addSubView, self);
self.collection.on("remove", self.removeSubView, self);
self.collection.on("reset", self.reset, self);
self.collection.on("closeAll", self.closeAll, self);
self.collection.reset = function(models, options) {
self.closeAll();
Backbone.Collection.prototype.reset.call(this, models, options);
};
self.reset();
},
reset: function() {
this.$el.empty();
this.render();
},
render: function() {
console.log("rendering viewcollection for",this.collection.models);
var self = this;
self.collection.each(function(model) {
self.addSubView(model);
});
return self;
},
addSubView: function(model) {
var sv = new this.subViewClass({model: model});
this.$el.append(sv.render().el);
},
removeSubView: function(model) {
model.trigger("close");
},
closeAll: function() {
this.collection.each(function(model) {
model.trigger("close");
});
}
});
Usage:
var PartView = SubView.extend({
tagName: "tr",
className: "part",
template: _.template($("#part-row-template").html())
});
var PartListView = ViewCollection.extend({
el: $("table#parts"),
subViewClass: PartView
});
Check out this mixin for creating and rendering subviews:
https://github.com/rotundasoftware/backbone.subviews
It is a minimalist solution that addresses a lot of the issues discussed in this thread, including rendering order, not having to re-delegate events, etc. Note that the case of a collection view (where each model in the collection is represented with one subview) is a different topic. Best general solution I am aware of to that case is the CollectionView in Marionette.
I don't really like any of the above solutions. I prefer for this configuration over each view having to manually do work in the render method.
views can be a function or object returning an object of view definitions
When a parent's .remove is called, the .remove of nested children from the lowest order up should be called (all the way from sub-sub-sub views)
By default the parent view passes it's own model and collection, but options can be added and overridden.
Here's an example:
views: {
'.js-toolbar-left': CancelBtnView, // shorthand
'.js-toolbar-right': {
view: DoneBtnView,
append: true
},
'.js-notification': {
view: Notification.View,
options: function() { // Options passed when instantiating
return {
message: this.state.get('notificationMessage'),
state: 'information'
};
}
}
}
Backbone was intentionally built so that there was no "common" practice in regards to this and many other issues. It is meant to be as unopinionated as possible. Theoretically, you don't even have to use templates with Backbone. You could use javascript/jquery in the render function of a view to manually change all of the data in the view. To make it more extreme, you don't even need one specific render function. You could have a function called renderFirstName which updates the first name in the dom and renderLastName which updates the last name in the dom. If you took this approach, it would be way better in terms of performance and you'd never have to manually delegate events again. The code would also make total sense to someone reading it (although it would be longer/messier code).
However, usually there is no downside to using templates and simply destroying and rebuilding the entire view and it's subviews on each and every render call, as it didn't even occur to the questioner to do anything otherwise. So that's what most people do for pretty much every situation they come across. And that's why opinionated frameworks just make this the default behavior.
You could also inject the rendered subviews as variables into the main template as variables.
first render the subviews and convert them to html like this:
var subview1 = $(subview1.render.el).html();
var subview2 = $(subview2.render.el).html();
(that way you could also dynamically string concatenate the views like subview1 + subview2 when used in loops) and then pass it to the master template which looks like this:
... some header stuff ...
<%= sub1 %>
<%= sub2 %>
... some footer stuff ...
and inject it finally like this:
this.$el.html(_.template(MasterTemplate, { sub1: subview1, sub2: subview2 } ));
Regarding the Events within the subviews: They will be most likely have to be connected in the parent (masterView) with this approach not within the subviews.
I like to use the following approach which also make sure to remove the child views properly. Here is an example from the book by Addy Osmani.
Backbone.View.prototype.close = function() {
if (this.onClose) {
this.onClose();
}
this.remove(); };
NewView = Backbone.View.extend({
initialize: function() {
this.childViews = [];
},
renderChildren: function(item) {
var itemView = new NewChildView({ model: item });
$(this.el).prepend(itemView.render());
this.childViews.push(itemView);
},
onClose: function() {
_(this.childViews).each(function(view) {
view.close();
});
} });
NewChildView = Backbone.View.extend({
tagName: 'li',
render: function() {
} });
There is no need to re-delegate events as it is costly. See below:
var OuterView = Backbone.View.extend({
initialize: function() {
this.inner = new InnerView();
},
render: function() {
// first detach subviews
this.inner.$el.detach();
// now can set html without affecting subview element's events
this.$el.html(template);
// now render and attach subview OR can even replace placeholder
// elements in template with the rendered subview element
this.$el.append(this.inner.render().el);
}
});
var InnerView = Backbone.View.extend({
render: function() {
this.$el.html(template);
}
});