Backbone Router and Views - backbone.js

I've been getting started with Backbone.js and trying to get my head around Routing to specific Views. In my HTML I have <a href="#project/1"> tags to render the View for the tasks of a project.
Query
When the link is clicked, it appends the task with the id to the DOM, however, when a second link is clicked, it appends that task underneath the previous. I'm not sure if its best practice to $.empty the View then call the show method?
A snippet of my Router:
routes: {
'project/:id: 'showtasks'
},
showtasks: function(id) {
Event.trigger('tasks:show', id);
}
Snippet of the Collection of Tasks
initialize: function() {
Event.on('tasks:show', this.show, this);
},
show: function() {
var task = this.collection.get(id);
var taskView = new App.Views.Task({ model: task });
this.$el.append(taskView.render().el);
}
The collection
var tasks = new App.Collections.Tasks([
{
id: 1,
title: 'First Task',
content: 'Lots of Content...'
},
{
id: 2,
title: 'Second Task',
content: 'Lots of Content...'
},
{
id: 3,
title: 'Third Task',
content: 'Lots of Content...'
}
]);
var tasksView = new App.Views.Tasks({ collection: tasks });

A couple of good design patterns for Backbone view is:
Calling render method multiple times should not have any side effect. It should render correctly.
When you use append in a render, you are basically setting up the flow of your view in the render method which should be basically the responsibility of the template of your view.
So I would suggest you should use this >
this.$el.html(taskView.render().el);
This would work perfectly fine however you would get into an issue if you have subviews. For that read this - (basically this whole answer is a shameless ripoff of this article :P )

Related

Backbone Marionette : Add a model (and render its view) to a nested Composite View

If you don't want to see the complete code, here is what I am trying to do.
I have multiple pages and each page has multiple tags. There is a composite View called PageManyView for rendering pages which called its childView PageView. Page View is a nested composite view which renders tags, passing this.model.get('tags') as collection.
Now I can easily add a new page by using pages.add(newPage). Here pages is the collection. I am facing problem in adding a new Tag. How can I do that. Please help.
CODE
var PageModel = Backbone.Model.extend({});
var PageCollection = Backbone.Collection.extend({
model: PageModel
});
My JSON at /data endpoint is coming like this
[
{
_id: '1', 'name': '1', info: 'Page 1',
tags: [{name:'main', color:'red'}, {name:'page', color:'blue'}]
},
{
_id: '1', 'name': '2', info: 'Page 2',
tags: [{name:'section', color:'blue'} {name:'about', color:'yellow'}]
}
]
I have created Nested Views in Marionette like this:
TagView = Marionette.ItemView.extend({
template: '#tagOneTemplate'
});
PageView = Marionette.CompositeView.extend({
template: '#pagesTemplate',
childViewContainer: 'div.tags',
childView: EntityViews.TagView,
initialize: function(){
var tags = this.model.get('tags');
this.collection = new Backbone.Collection(tags);
}
});
PageManyView = Marionette.CompositeView.extend({
template: '#pageManyTemplate',
childView: EntityViews.PageView,
childViewContainer: 'div#all-pages'
});
Now here is where i am facing problem. Inside Controller of my application, lets say if I have to add a new page
showPages: function(){
//Getting pages by using jQuery deferred
var view = PageMainView({collection:pages});
view.on("add:page", function(){
var newPage = Page({_id: 3});
pages.add(newPage);
});
}
Now this add function renders the new page automatically.
BUT I AM FACING PROBLEM IN ADDING a NEW TAG. HOW CAN I ADD A NEW TAG?
Finally it worked. Here is what I have done.
Step 1: Get Current model (page) from pages collection.
var currentpage = pages.get(pageid);
Step 2: Use Marionette BabySitter to get the view of the page where I want to insert a new tag.
var v = view.children.findByModel(currentpage);
Step 3: Add tag to v.collection. Since v is the View of the page where I want to insert new tag, v.collection returns the initialised tags collection
v.collection.add(tag);
This works for me. Let me know if I am wrong somewhere or a better way exists. Hope it helps.
this can be done quite easily by shifting around how your collection is being passed in. Instead of setting the collection on initialize in your compositeView, you should pass it in directly during instantiation. This way when you make a change to the collection from within your model, the compositeView will hear the "add" event on collection and add node automagically for you
For example it might look something like this.
PageView = Marionette.CompositeView.extend({
template: '#pagesTemplate',
childViewContainer: 'div.tags',
childView: EntityViews.TagView,
});
new PageView({
model: myModel,
collection: myModel.get("tags")
});
myModel.get("tags").add([{new: "object"}])

Creating a sidebar using backbone.js

I want to create a sample page using backbone.js and i am completely new to it.
The view should be similar to the shown in backbone.js website where you have a sidebar and on the right side you have a content.
As shown in this link:-
http://backbonejs.org/
Consider the following scenario:-
var model = Backbone.Model.extend({
data:[
{'title': 'Apple', 'logo':'images/apple.png', 'history': "Some history about apple"},
{'title': 'Google', 'logo':'images/google.png', 'history': "Some history about google"},
{'title': 'Microsoft', 'logo':'images/mirosoft.png', 'history': "Some history about microsoft"}
]
]
});
Now in my view the sidebar should contains links in list form like
Apple
Google
Microsoft
and on the right side, I want to see apple logo and history which passed in the data above.
Similarly when somebody clicks on google on the runtime, I change the content on right side of sidebar
Any guidance on how to get this..?
See the answer below to get the basic idea about passing parameters between views:
Event handling between views
Basically, you have to register an event which when fired, will re-display your main content:
ContentView = Backbone.View.extend({
initialize: function () {
App.vent.on('show', this.show, this);
},
show: function (company) {
// do something with your model, then call render()
}
// ....
});
and a sidebar view which is supposed to trigger that event when you click on any of the links:
SidebarView = Backbone.View.extend({
events: {
'.companylink click': 'clicked'
},
clicked: function (company) {
App.vent.trigger('show', company);
}
// .....
});

Backbone stickit selectOptions from ajax

I am trying to populate a select using below.
View: Backbone.View.extend({
initialize: function () {
this.setElement($('#templatePlaceholder'));
},
bindings: {
'select#List': {
observe: 'ddList',
selectOptions: {
collection: Q.when(getSelects.execute()).then(function (data) {
console.dir(data);
return data;
})
}
}
},
console returns an array object:
[ { value: 1, label: "a" }, { value: 2, label: "b" } ]
But I get nothing.
If I define collection as:
collection: function () { return [{ value: 1, label: "a" },{ value: 2, label: "b" }];}
then all works fine.
There is potentially a couple of problems with this: that asynchronous code is executing immediately as View is being assigned which may not be desired; and collection is getting assigned to the result of executing that promise, not whatever is being returned by return data.
Some solutions:
1) You could bootstrap your select options on page load. This is a popular pattern in single page or Backbone apps. Basically, you have the backend put together json data-structures of common data and then render it to the page as a javascript variable. So the server side template (jsp, erb, php) would look something like this:
<!DOCTYPE html>
...
<script type="text/javascript">
window.bootstrap = { users: <%= userjson %>, ddlist: <%= ddlistjson %> };
</script>
...
This method is recommended for performance and convenience.
2) If you have the option to run getSelects.execute() synchronously - for example if it is a jQuery ajax request, run it with the async:false option - then you could execute it in a couple of places in the view. During initialize will fetch the data when the view is actually constructed. Or you could assign your collection binding with a function that runs the getSelects routine, which blocks and then returns the data:
collection: function() {
var collection = [];
// Assumes that getSelects.execute() will block and run synchronously.
Q.when(getSelects.execute()).then(function (data) {
collection = data;
});
return collection;
}
This method is not recommended as it could leave your UI in a funky state as it blocks other javascript from running, and could potentially get hung up.

Backbone Layout Manager subview's events doesn't work after reload

I'm using the Backbone Layout Manager Boilerplate. Unfortunately, a quite frustrating bug occurred. I like render a list of items as subviews inserted by insertView function. At the first load everthing works fine. But after a reload the the click events doesn't work anymore :(. I already tried to call delegateEvents() on the TableItem View manually but nothing changed. I hope anyone can give me a clue.
App.Views.Item = Backbone.View.extend({
template: "templates/item",
tagName: "li",
events: {
"click .applyButton" : "apply",
"click .viewDetailsButton" : "showDetail"
},
serialize: function() {
return { table : this.model.toJSON() };
},
apply: function(ev) {
ev.preventDefault();
alert("apply button clicked");
},
showDetail: function(ev) {
ev.preventDefault();
var id = this.model.get("_id");
app.router.navigate("#events/"+ id, {trigger : true})
}
});
/*
* List View
*/
App.Views.List = Backbone.View.extend({
template: "templates/list",
tagNam: "ul",
className: "tableList",
beforeRender: function() {
var events = this.model.get("userEvents").get("hosting");
events.each(function(model) {
this.insertView(new App.Views.Item({ model : model }));
}, this);
},
serialize: function() {
return {};
}
});
I think you might want to add a cleanup function on your Item view to undelegate the events when layoutmanager removes the view. I don't know if this will fix your problem, but it seems like good practise.
When you say after a reload, do you mean reloading the page with the browser reload button? if so, how do you get it to work in the first place?
It would help if you could provide a jsfiddle of your setup, or point us to a repo so we can test it on our machines. Make sure you include the router so that we can have a look at how the view and the layout that contains it are initialised.

loading multiple views in init function breaks one or the other

I'm building my first backbone.js app, and I've run into a problem when trying to initialize my app and display both recipes and a shopping list, both of which are different (yet related) backbone objects.
My init function is
var MyApp= {
Models: {},
Views: {},
Routers: {},
Collections: {},
AppView: {},
Init: function() {
new MyApp.Views.ShoppingList;
new MyApp.Routers.Recipes;
Backbone.history.start();
}
};
Strangely, when I use
new MyApp.Routers.ShoppingList;
new MyApp.Routers.Recipes;
I don't get the shopping list View, I only get the recipes.
I also don't get any errors.
The shopping list router is fairly basic
MyApp.Routers.ShoppingList = Backbone.Router.extend({
routes: {
"": "index",
"shopping_list/:id": "show"
},
index: function(){
console.log('this');
new MyApp.Views.ShoppingList();
}
});
so from what I understand, the app should load the router, and display the view, but I'm not getting that or the console.log.
--------------as requested, here is my 'recipes router'---------------
MyApp.Routers.Recipes = Backbone.Router.extend({
routes: {
"": "index",
"recipes/:id": "show"
},
index: function(){
if(!MyApp.RecipeList){
MyApp.RecipeList = new MyApp.Collections.RecipeList;
MyApp.RecipeList.page = 1;
} else {
MyApp.RecipeList.page++;
}
MyApp.RecipeList.url='/recipes?page='+MyApp.RecipeList.page;
MyApp.RecipeList.fetch({
add: true,
success: function() {
new MyApp.Views.RecipeList({ collection: MyApp.RecipeList});
},
error: function() {
new Error({ message: "Error loading documents." });
}
});
},
show: function(id){
var recipe = MyApp.RecipeList.get(id);
new MyApp.Views.RecipeView({ model: recipe});
},
newRecipe: function(){
new App.Views.Edit({ model: new Recipe() });
},
edit: function(id){
var recipe = new Recipe({ id: id});
recipe.fetch({
success: function(model, resp){
new App.Views.Edit({ model: recipe});
},
error: function(){
new Error({message: "Hey!? Were'd it go? sorry I can't find your recipe"});
window.location.hash = '#';
}
});
}
});
----------------- some progress -----------------------------
I may be wrong, but in commenting out sections of the router, I find that the problem may be caused by my 'routes' as they both have index where the url is empty. Commenting out the 'routes' in one controller/router causes the other controller/router to display.
I've changed the routes so that they are more representative of their namespace
routes{
"recipes" : "recipes"
},
recipes: function()...
but I'm still not getting the right information to display. I'm now trying to figure out if I need an initialize function and what that would look like, or if I've even debugged this properly
--------------------- update, I was using backbone wrong ------------------------
It turns out I believe that I was mis-understanding Routers and was thinking of them more like controllers, so I was calling multiple routers on load, but the page was only loading the last one which pointed to an empty route as you can only request a single url route at a time.
Now I'm loading multiple Views on load and only one router.
After instantiating your view, you still need to render it and add it to the DOM.
index: function(){
console.log('this');
var view = new MyApp.Views.ShoppingList();
//you don't have to append to the whole body, but this is just an example
$('body').append(view.render().el);
}

Resources