How should I be handling routing in BackboneJS? When routing, after new-upping my view, should I be triggering an event, or rendering the view directly?
Here are the two scenarios:
Trigger Event:
routes: {
'orders/view/:orderId' : 'viewOrder'
},
viewOrder: function (orderId) {
var viewOrderView = new ViewOrderView();
vent.trigger('order:show', orderId);
}
In my view, I have:
var ViewOrderView = Backbone.View.extend({
el: "#page",
initialize: function () {
vent.on('order:show', this.show, this);
},
show: function (id) {
this.id = id;
this.render();
},
render: function () {
var template = viewOrderTemplate({ id: this.id });
this.$el.html(template);
return this;
}
});
OR, should I go this route:
routes: {
'orders/view/:orderId' : 'viewOrder'
},
viewOrder: function (orderId) {
var viewOrderView = new ViewOrderView({id : orderId });
viewOrderView.render();
}
In my view, I have:
var ViewOrderView = Backbone.View.extend({
el: "#page",
initialize: function () {
//init code here
},
render: function () {
var template = viewOrderTemplate({ id : this.id});
this.$el.html(template);
return this;
}
});
I think it's the first scenario - given that backbone is event driven, but the 2nd obviously has less code.
Also, I suppose a third scenario would be to keep the view code in the first scenario, but grab the router scenario of the second... rendering the view on navigation, but exposing an event in case I want to trigger that elsewhere.
Thoughts?
So all backbone questions usually end up with many plausible answers. In this case, I believe your second example is a more canonical/typical backbone pattern. Putting aside the tricky issue of handling loading spinners and updating after data loads, the simplified basic pattern in your router would be:
routes: {
'orders/view/:orderId' : 'viewOrder'
},
viewOrder: function (orderId) {
//Use models to represent your data
var orderModel = new Order({id: orderId});
//models know how to fetch data for themselves given an ID
orderModel.fetch();
//Views should take model instances, not scalar model IDs
var orderView = new OrderView({model: orderModel});
orderView.render();
//Exactly how you display the view in the DOM is up to you
//document.body might be $('#main-container') or whatever
$(document.body).html(orderView.el);
}
I think that's the textbook pattern. Again, the issue of who triggers the fetching of data and rerendering after it arrives is tricky. I think it's best if the view knows how to render a "loading" version of itself until the model has fetched data, and then when the model fires a change event after fetch completes, the view rerenders itself with the loaded model data. However, some people might put that logic elsewhere. This article on building the next soundcloud I think represents many very good "state of the art" backbone patterns, including how they handle unfetched models.
In general, you can code things with callbacks or events as you prefer. However, a good rule of thumb is to ask yourself some questions:
Is more than one independent logical piece of work going to respond to this event?
Do I need to decouple the source of this event from the things that happen in response to it?
If both of those are "yes", then events should be a good fit. If both are "no", than straightforward function logic is a better fit. In the case of "navigating to this URL triggers this view", generally the answer to both questions is "no", so you can just code that logic into the router's route handler method and be done with it.
I'd use second scenario. Don't see any benefits of using first approach. It would make more sence this way (but still arguable):
/* ... */
routes: {
'orders/view/:orderId' : 'viewOrder'
},
viewOrder: function (orderId) {
vent.trigger('order:show', orderId);
}
/* ... */
vent.on('order:show', function(orderId) {
var viewOrderView = new ViewOrderView();
viewOrderView.render();
});
var ViewOrderView = Backbone.View.extend({
el: "#page",
initialize: function (options) {
this.orderId = options.orderId;
},
render: function () {
var template = viewOrderTemplate({
id: this.orderId
});
this.$el.html(template);
return this;
}
});
This way at least you'd be able to trigger route action without updating a url. But same effect might be achieved using Backbone.router.viewOrder(1) probably. Events are pretty powerful, but i wouldn't use them if i don't really need.
Related
I ran into this article (http://coenraets.org/blog/2012/01/backbone-js-lessons-learned-and-improved-sample-app/) and was wondering if the idea of binding and rendering views in the router after instantiating them is best practice. I have been binding my views and rendering them in my view definition.
Currently this is how I've been setting up and calling my views:
EmployeeView:
EmployeeView = Backbone.View.extend({
el: '#content',
template:template,
initialize: function () {
this.collection.fetch({
reset: true
});
this.collection.on('reset',this.render, this);
},
render: function(){
this.el.innerHTML = Mustache.to_html(this.template, { employee_list: this.collection.toJSON()});
console.log('render called');
}
My Router:
employeeList: function () {
var c = new EmployeeCollection
new EmployeeView( {
collection: c
});
}
It works fine. But according to the article a better practice is to do the following:
EmployeeView = Backbone.View.extend({
template:template,
initialize: function () {
this.collection.fetch({
reset: true
});
this.collection.on('reset',this.render, this);
},
render: function(){
this.el.innerHTML = Mustache.to_html(this.template, { employee_list: this.collection.toJSON()});
console.log('render called');
return this;
}
Router
employeeList: function () {
var c = new EmployeeCollection
$('#content').html(new EmployeeView( {collection: c}).render().el);
},
I like the solution in the article because it decouples the views from other DOM events as the article said and allows me to focus all my tweaking and customizing in one place, the router. But because I'm passing in a collection/model and need to fetch the data in my initialize my page renders twice. My questions are:
Is this really best practice?
How do I avoid calling the render twice if I want to use the suggested method?
What if I have cases where I have some front end user interaction and then need to refresh the view collection/model? Would I have to do it in my view or could that happen in the router as well?
The view you have here, and the one in the article are totally different.
In your example, the view is bound to an element in DOM (#content),
which is not a good practice, especially for beginners and causes lots of bugs that we see here every day.
For example if you create 2 instances of your view then event will starts firing multiples times and along with that all hell will break loose.
The view in the article creates a new <div> element in memory per instance, which is a good practice.
Now, to add this in DOM, newbies often do stuff like the following inside the view's render:
$('#content').html(this.$el);
This creates a global selector inside the view and makes it aware of the outer world which is not a good practice.
The article probably (I didn't read it) address this is issue and presents and alternative of adding the view element to DOM from the router, which is a good practice in my opinion.
To avoid rendering twice in the code from article you can just do:
$('#content').html(new EmployeeView( {collection: c}).el);
el being a live reference, it'll be updated when the fetch succeeds. .render().el is another common mis-understanding spread by all the existing blogs and tutorials.
Side note: Since we are discussing best practices, omitting the semicolon and parenthesis as in var c = new EmployeeCollection is not a good practice either. Go with var c = new EmployeeCollection();
You got it almost right. You're just rendering it twice, which I don't think is the right way to go, as there is no point.
EmployeeView = Backbone.View.extend({
template:template,
initialize: function(){
console.log("Will print second");
this.collection.fetch({ reset: true });
this.collection.on('reset', this.appendEmployees, this);
},
render: function(){
//this.el.innerHTML = Mustache.to_html(this.template, { employee_list: this.collection.toJSON()});
console.log('Will print 3rd. render called');
return this;
}
appendEmployees: function(){
console.log("Will print 4th. Appending employees");
$(this.el).html(Mustache.to_html(this.template, {employee_list: this.collection.toJSON() });
}
})
Router
employeeList: function () {
var c = new EmployeeCollection()
var view = new EmployeeView({ collection: c });
console.log("Will print 1st");
$('#content').html(view.render().el);
}
First, when you do view.render().el it will append view's element (which will be empty by that time) to #content
Second, you're executing appendEmployees function when collection resets. By the time this will happen your element will already be placed in the DOM.
In case you need to refresh, it can be done inside the view, by calling the appendEmployees function, or even by resetting your collection. Or if you navigate to the same route via backbone, the whole process will be repeated hence your collection will be called again, and the page will render from beginning. So it comes down to your preferences on when/why you'd choose one over the other. Hope this helps.
I have a marionette view that have a method to create a new model from a bootbox. Now i need to be able to edit the model from the bootbox, how can i I pass the current model data to the box?
This is some of my current code:
Module.Views.Chaptersx = Marionette.CompositeView.extend({
template: Module.Templates['documents/create/course/chapter/index'],
childView: Module.Views.ChapterItemx,
childViewContainer: "#chaptersCollection",
events: {
'click .chapters-create': 'create',
//'click #uploadFilesChapters': 'startUpload'
},
create: function (evt) {
console.log('create');
evt.preventDefault();
var me = this;
var box = bootbox.dialog({
show: false,
title: "Nueva Seccion",
message: Module.Templates['documents/create/course/chapter/chapterModal'],
buttons: {
success: {
label: "Guardar",
className: "btn-success",
callback: function () {
var chapterNo = $('#cn').val();
var chapterName = $('#chapterName').val();
var chapter = new Module.Models.Chapter({
chapterNo: chapterNo,
chapterName: chapterName,
});
me.collection.add(chapter);
}
}
}
});
box.on("show.bs.modal", function () {
console.log('numbers');
var number = (me.collection.size() + 1);
$('#cn').val(number);
});
box.modal('show');
},
TL;DR - use model's custom events or an event bus to pass the data.
You can reference this.model in the view, which is somewhat of a compromise (you're tying the view and the model).
You could pass the data via the event object's data property, but for that you're gonna have to extend some methods and get into backbone's nitty gritty.
Use a data- attribute on the element:
<div class="chapters-create" data-cats></div>
create: function (evt) {
var cats = $(evt.currentTarget).data('cats');
// ...
}
… which is considered bad habit by the way - you're still tying data to the DOM (or model to view, MVC speaking).
Well, I don't like either of the above, as they tend to have high coupling - I'd do it with custom events on a shared model resides at a higher level.
I don't know where the data comes from, but bottom line - shoot it in a custom event, or, better yet, use an event bus, like the one offered by marionette.js.
You need to create another view, call it EditView or something, render it, and provide the view.el as a message option to bootbox. However, the whole thing feels like a hack to me, and I think that it's better to implement a modalRegion and manage the modals yourself.
so i had the same famous problem of zombie views in my backbone app. I tried this to become a superhero :P
var Router=Backbone.Router.extend({
routes:{
"":"loadDashboard",
"home":"loadDashboard",
'post-leads':"loadPostLeads"
},
initialize:function(){
window.currentView=null;
},
loadPostLeads:function(){
require(['views/post-leads'],function(leads){
if(window.currentView!=null)
{window.currentView.remove();}
window.currentView=new leads();
window.currentView.render();
})
},
loadDashboard: function(){
require(['views/dashboard'],function(dashboard){
if(window.currentView!=null)
{window.currentView.remove();}
window.currentView=new dashboard();
window.currentView.render();
})
}
});
This doesn't work. I wanted something simple and don't want to use marionette or anything similar for that sake. Whats going wrong above? Is it a sensible approach?
In principle what you do should work, but there are some things that Backbone can't clean up, because it doesn't know of them.
First, you should make sure that you are using a recent version of Backbone (0.9.9 or newer). There have been some improvements to the event binding code, which makes it easier for the View.remove method to do all the necessary cleanup.
The common gotchas are:
Listening to model events:
//don't use other.on (Backbone doesn't know how to clean up)
this.model.on('event', this.method);
//use this.listenTo (Backbone cleans up events when View.remove is called)
//requires Backbone 0.9.9
this.listenTo(this.model, 'event', this.method);
Listening to DOM events outside your view's scope:
//if you listen to events for nodes that are outside View.el
$(document).on('event', this.method);
//you have to clean them up. A good way is to override the View.remove method
remove: function() {
$(document).off('event', this.method);
Backbone.View.prototype.remove.call(this);
}
Direct references:
//you may hold a direct reference to the view:
this.childView = otherView;
//or one of its methods
this.callback = otherView.render;
//or as a captured function scope variable:
this.on('event', function() {
otherView.render();
});
Closures:
//if you create a closure over your view, or any method of your view,
//someone else may still hold a reference to your view:
method: function(arg) {
var self = this;
return function() {
self.something(x);
}
}
Avoiding the following pitfalls should help your views to get cleaned up correctly.
Edit based on comment:
Ah, you didn't mention the full problem in your question. The problem with your approach is, as I gather, is that you're trying to render the two views into the same element:
var View1 = Backbone.View.extend({el:"#container" });
var View2 = Backbone.View.extend({el:"#container" });
And when you remove View1, the View2 does not correctly render.
Instead of specifying the view el, you should render the views into an element. On your page you should have a #container element, and append the view's element into the container.
loadPostLeads: function () {
var self = this;
require(['views/post-leads'], function (leads) {
self.renderView(new leads());
})
},
loadDashboard: function () {
var self = this;
require(['views/dashboard'], function (dashboard) {
self.renderView(new dashboard());
})
},
renderView: function(view) {
if(window.currentView) {
window.currentView.remove();
}
//the view itself does not specify el, so you need to append the view into the DOM
view.render();
$("#container").html(view.el);
window.currentView = view;
}
I am trying to create my first backbone app and am having some difficulty getting my head around how I am meant to be using views.
What I am trying to do is have a search input that each time its submitted it fetches a collection from the server. I want to have one view control the search input area and listen to events that happen there (a button click in my example) and another view with sub views for displaying the search results. with each new search just prepending the results into the search area.
the individual results will have other methods on them (such as looking up date or time that they where entered etc).
I have a model and collection defined like this:
SearchResult = Backbone.model.extend({
defaults: {
title: null,
text: null
}
});
SearchResults = Backbone.Collection.extend({
model: SearchResult,
initialize: function(query){
this.query = query;
this.fetch();
},
url: function() {
return '/search/' + this.query()
}
});
In my views I have one view that represents the search input are:
var SearchView = Backbone.View.extend({
el: $('#search'),
events: {
'click button': 'doSearch'
},
doSearch: function() {
console.log('starting new search');
var resultSet = new SearchResults($('input[type=text]', this.el).val());
var resultSetView = new ResultView(resultSet);
}
});
var searchView = new SearchView();
var ResultSetView = Backbone.View.extend({
el: $('#search'),
initialize: function(resultSet) {
this.collection = resultSet;
this.render();
},
render: function() {
_(this.collection.models).each(function(result) {
var resultView = new ResultView({model:result});
}, this);
}
});
var ResultView = Backbone.view.extend({
tagName: 'div',
model: SearchResult,
initialize: function() {
this.render();
},
render: function(){
$(this.el).append(this.model.get(title) + '<br>' + this.model.get('text'));
}
});
and my html looks roughly like this:
<body>
<div id="search">
<input type="text">
<button>submit</button>
</div>
<div id="results">
</div>
</body>
In my code it gets as far as console.log('starting new search'); but no ajax calls are made to the server from the initialize method of the ResultSetView collection.
Am I designing this right or is there a better way to do this. I think because the two views bind to different dom elements I should not be instantiating one view from within another. Any advice is appreciated and if I need to state this clearer please let me know and I will do my best to rephrase the question.
Some problems (possibly not the only ones):
Your SearchView isn't bound to the collection reset event; as written it's going to attempt to render immediately, while the collection is still empty.
SearchView instantiates the single view ResultView when presumably it should instantiate the composite view ResultSetView.
You're passing a parameter to the SearchResults collection's constructor, but that's not the correct way to use it. See the documentation on this point.
You haven't told your ResultSetView to listen to any events on the collection. "fetch" is asynchronous. When completed successfully, it will send a "reset" event. Your view needs to listen for that event and then do whatever it needs to do (like render) on that event.
After fixing all the typos in your example code I have a working jsFiddle.
You see like after clicking in the button an AJAX call is done. Of course the response is an error but this is not the point.
So my conclusion is that your problem is in another part of your code.
Among some syntax issues, the most probable problem to me that I see in your code is a race condition. In your views, you're making an assumption that the fetch has already retrieved the data and you're executing your views render methods. For really fast operations, that might be valid, but it gives you no way of truly knowing that the data exists. The way to deal with this is as others have suggested: You need to listen for the collection's reset event; however, you also have to control "when" the fetch occurs, and so it's best to do the fetch only when you need it - calling fetch within the search view. I did a bit of restructuring of your collection and search view:
var SearchResults = Backbone.Collection.extend({
model: SearchResult,
execSearch : function(query) {
this.url = '/search/' + query;
this.fetch();
}
});
var SearchView = Backbone.View.extend({
el: $('#search'),
initialize : function() {
this.collection = new SearchResults();
//listen for the reset
this.collection.on('reset',this.displayResults,this);
},
events: {
'click button': 'doSearch'
},
/**
* Do search executes the search
*/
doSearch: function() {
console.log('starting new search');
//Set the search params and do the fetch.
//Since we're listening to the 'reset' event,
//displayResults will execute.
this.collection.execSearch($('input[type=text]', this.el).val());
},
/**
* displayResults sets up the views. Since we know that the
* data has been fetched, just pass the collection, and parse it
*/
displayResults : function() {
new ResultSetView({
collection : this.collection
});
}
});
Notice that I only created the collection once. That's all you need since you're using the same collection class to execute your searches. Subsequent searches only need to change the url. This is better memory management and a bit cleaner than instantiating a new collection for each search.
I didn't work further on your display views. However, you might consider sticking to the convention of passing hashes to Backbone objects. For instance, in your original code, you passed 'resultSet' as a formal parameter. However, the convention is to pass the collection to a view in the form: new View({collection: resultSet}); I realize that that's a bit nitpicky, but following the conventions improves the readability of your code. Also, you ensure that you're passing things in the way that the Backbone objects expect.
I have the following problem…
MyView which is connected to two views: TaskModel and UserModel
TaskModel = {id: 1, taskName: "myTask", creatorName: "myName", creator_id: 2 },
UserModel = {id: 2, avatar: "someAvatar"}
The view should display
{{taskName}}, {{creatorName}}, {{someAvatar}}
As you can see the fetch of TaskModel and UserModel should be synchronized, because the userModel.fetch needs of taskModel.get("creator_id")
Which approach do you recommend me to display/handle the view and the two models?
You could make the view smart enough to not render until it has everything it needs.
Suppose you have a user and a task and you pass them both to the view's constructor:
initialize: function(user, task) {
_.bindAll(this, 'render');
this.user = user;
this.task = task;
this.user.on('change', this.render);
this.task.on('change', this.render);
}
Now you have a view that has references to both the user and the task and is listening for "change" events on both. Then, the render method can ask the models if they have everything they're supposed to have, for example:
render: function() {
if(this.user.has('name')
&& this.task.has('name')) {
this.$el.append(this.template({
task: this.task.toJSON(),
user: this.user.toJSON()
}));
}
return this;
}
So render will wait until both the this.user and this.task are fully loaded before it fills in the proper HTML; if it is called before its models have been loaded, then it renders nothing and returns an empty placeholder. This approach keeps all of the view's logic nicely hidden away inside the view where it belongs and it easily generalizes.
Demo: http://jsfiddle.net/ambiguous/rreu5jd8/
You could also use Underscore's isEmpty (which is mixed into Backbone models) instead of checking a specific property:
render: function() {
if(!this.user.isEmpty()
&& !this.task.isEmpty()) {
this.$el.append(this.template({
task: this.task.toJSON(),
user: this.user.toJSON()
}));
}
return this;
}
That assumes that you don't have any defaults of course.
Demo: http://jsfiddle.net/ambiguous/4q07budc/
jQuery's Deferreds work well here. As a crude example:
var succesFunction = function () {
console.log('success');
};
var errorFunction = function () {
console.log('error');
};
$.when(taskModel.fetch(), userModel.fetch()).then(successFunction, errorFunction);
You could also pipe the request through using the crude data (remember that fetch, save, create are really just wrappers around jQuery's $.ajax object.
var taskModelDeferred = taskModel.fetch();
var userModelDeferred = taskModelDeferred.pipe(function( data ) {
return userModel.fetch({ data: { user: data.userId }});
});
note: Backbone returns the collection and model in the success / error functions by default on collections and models so if you need this be sure have a reference handy.
I've run into this very same issue with a complex layout that used two models and multiple views. For that, instead of trying to synchronize the fetches, I simply used the "success" function of one model to invoke the fetch of the other. My views would listen only to the change of the second model. For instance:
var model1 = Backbone.Model.extend({
...
});
var model2 = Backbone.Model.extend({
...
});
var view1 = Backbone.View.extend({
...
});
var view2 = Backbone.View.extend({
...
});
model2.on("change",view1.render, view1);
model2.on("change",view2.render, view2);
Then...
model1.fetch({
success : function() {
model2.fetch();
}
});
The point to this is you don't have to do any sophisticated synchronization. You simply cascade the fetches and respond to the last model's fetch.