I have the following views in my application. Basically I want to call show_house() in App.MapView when the li of the App.HouseListElemView is clicked.
What would be the best way of doing this?
App.HouseListElemView = Backbone.View.extend({
tagName: 'li',
events: {
'click': function() {
// call show_house in App.MapView
}
},
initialize: function() {
this.template = _.template($('#house-list-template').html());
this.render();
},
render: function() {
var html = this.template({model: this.model.toJSON()});
$(this.el).append(html);
},
});
App.MapView = Backbone.View.extend({
el: '.map',
events: {
'list_house_click': 'show_house',
},
initialize: function() {
this.map = new GMaps({
div: this.el,
lat: -12.043333,
lng: -77.028333,
});
App.houseCollection.bind('reset', this.populate_markers, this);
},
populate_markers: function(collection) {
_.each(collection.models, function(house) {
var html = 'hello'
this.map.addMarker({
lat: house.attributes.lat,
lng: house.attributes.lng,
infoWindow: {
content: html,
}
});
}, this);
},
show_house: function() {
console.log('show house');
}
});
The current house is really part of your application's global state so create a new model to hold your global application state:
var AppState = Backbone.Model.extend({ /* maybe something in here, maybe not */ });
var app_state = new AppState;
Then your HouseListElemView can respond to clicks by setting a value in app_state:
App.HouseListElemView = Backbone.View.extend({
//...
events: {
'click': 'set_current_house'
},
set_current_house: function() {
// Presumably this view has a model that is the house in question...
app_state.set('current_house', this.model.id);
},
//...
});
and then your MapView simply listens for 'change:current_house' events from app_state:
App.MapView = Backbone.View.extend({
//...
initialize: function() {
_.bindAll(this, 'show_house');
app_state.on('change:current_house', this.show_house);
},
show_house: function(m) {
// 'm' is actually 'app_state' here so...
console.log('Current house is now ', m.get('current_house'));
},
//...
});
Demo: http://jsfiddle.net/ambiguous/sXFLC/1/
You might want current_house to be an actual model rather than simply the id of course but that's easy.
You'll probably be able to find all sorts of other uses for app_state once you have it. You can even add a little bit of REST and AJAX and get persistence for your application settings pretty much for free.
Events are the usual solution to every problem in Backbone and you can make models for anything you want, you can even make temporary models strictly for gluing things together.
Related
I have issue in rendering Shopping Bag Views using Backbone for my website.
I am using 1 collection for all Bag Views (“Quick_View” of items list & “Normal_View” of Items list). Also I created “Item_View”, which is being used to render each item in both the Views.
It is a SPA (Single Page Application) and “Quick_View” is initiated and rendered for all Backbone routes and hidden by default. Whenever user clicks on “Quick_View” link from any page it is showing. There is no route defined for it.
The “Normal_View”, can be accessed using Checkout button given in “Quick_View”. It is bind with “domain/checkout” route.
When I access “Normal_View” from “Quick_View” check button; it works fine and both (Quick and Normal) views are in Sync. Means, when we add, delete, update any item in any of the View, both Views are getting updated accordingly.
But when I access “domain/checkout” route directly in a new browser, both views are getting rendered fine, but they are not in sync. Means, change in 1 view does not update another view.
The reason, I tracked is, when I access “Normal_View” through “Quick_View”, model for each item in both the Views having same CID, so the both Views are in sync, if there is any change in a model from any of the View.
And, when I access “Normal_View” directly, model for each item in both the views are not having same CID, so they do not work as expected.
There are few more points to consider:
Collection is firing reset event twice for “Quick_View” and each item
in “Quick_View” is rendering twice.
When, I access “Normal_View” (in either way), “Quick_View” is again getting rendered but once “Normal_View” rendering is over.
// Main View
var mainView = Backbone.View.extend({
el: 'body',
template: {
header: Handlebars.compile(headerTemplate),
mainNav: Handlebars.compile(mainNavtemplate),
footer: Handlebars.compile(footerTemplate)
},
initialize: function() {
_.bindAll();
AW.collection.bag = new bagCollection();
//AW.collection.bag.fetch({reset:true});
},
render: function() {
this.$el.html(this.template());
this.loadSubView('bagQV');
},
loadSubView: function(subView) {
switch(subView) {
case 'home' :
if(!AW.view.home) AW.view.home = new homepageView();
AW.view.home.render();
break;
case 'bagQV' :
if(!AW.view.bagQV) AW.view.bagQV = new bagQuickView({collection: AW.collection.bag});
//AW.view.bagQV.render();
break;
case 'checkout' :
if(!AW.view.checkout) AW.view.checkout = new checkoutView({collection: AW.collection.bag});
AW.view.checkout.render();
break;
}
}
});
// Single Item View
var bagItemView = Backbone.View.extend({
tagName: 'tr',
template: Handlebars.compile(bagItemTemplate),
initialize: function() {
_.bindAll(this);
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'remove', this.removeItem);
$(document).on('keyup', this.listenKeyboard);
},
events: {
'click .qtyInput .button' : 'updateItem',
'click .controls a.remove' : 'removeModel'
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
this.$el.attr('data-id',this.model.cid);
return this;
},
updateItem: function(e) {
e.preventDefault();
e.stopPropagation();
var newQty = this.$el.find('.qtyInput input').val();
var newAmt = newQty * parseFloat(this.model.get('prodRate').replace('$',''));
this.model.set({prodQty: newQty, amount: '$' + newAmt});
this.cancelEdit(e);
},
removeModel: function(e) {
e.preventDefault();
e.stopPropagation();
if(AW.collection.bag) AW.collection.bag.remove(this.model);
},
removeItem: function() {
this.$el.remove();
}
});
// Bag Quick View
var bagQuickView = Backbone.View.extend({
tagName: 'div',
id: 'myBagQV',
template: Handlebars.compile(bagQuickViewTemplate),
initialize: function() {
_.bindAll(this);
this.collection.fetch({reset:true});
//this.collection.bind("reset", _.bind(this.render, this));
this.listenTo(this.collection, 'add', this.addItem);
this.listenTo(this.collection, 'reset', this.render);
},
render: function() {
if($('#myBagQV').length == 0) {
this.$el.html(this.template());
$('body').append(this.el);
}
this.addAllItems();
return this;
},
addItem: function(item) {
var parent = this;
var itemView = new bagItemView({model: item});
$('#itemsInBag table tbody').append(itemView.render().el);
},
addAllItems: function() {
if(this.collection.length > 0) {
$('#itemsInBag table tbody').html('');
this.collection.each(this.addItem, this);
}
},
});
// Normal Bag View
var bagView = Backbone.View.extend({
tagName: 'div',
id: 'myBag',
template: Handlebars.compile(checkoutBagTemplate),
initialize: function() {
_.bindAll(this);
this.collection.fetch({reset:true});
//this.collection.bind("reset", _.bind(this.render, this));
this.listenTo(this.collection, 'add', this.addItem);
this.listenTo(this.collection, 'reset', this.render);
},
render: function() {
this.$el.html(this.template());
$('#checkoutContainer #details').append(this.el);
this.addAllItems();
return this;
},
addItem: function(item) {
var parent = this;
var itemView = new bagItemView({model: item});
this.$el.find('table tbody').append(itemView.render().el);
},
addAllItems: function() {
if(this.collection.length > 0) {
this.$el.find('table tbody').html('');
this.collection.each(this.addItem, this);
}
}
});
Looking for you help.
Thank you in advance
Cheers,
Vikram
Here's my Backbone.js:
(function() {
window.App = {
Models: {},
Collections: {},
Views: {},
Router: {}
};
window.template = function(id) {
return _.template( $('#' + id).html() );
};
var vent = _.extend({}, Backbone.Events);
App.Router = Backbone.Router.extend({
routes: {
'' : 'index',
'*other' : 'other'
},
index: function() {
},
other: function() {
}
});
App.Models.Main = Backbone.Model.extend({
defaults : {
FName: ''
}
});
App.Collections.Mains = Backbone.Collection.extend({
model: App.Models.Main,
initialize: function() {
this.fetch({
success: function(data) {
console.log(data.models);
}
});
},
url: '../leads/main_contact'
});
App.Views.Mains = Backbone.View.extend({
tagName: 'ul',
initialize: function() {
this.collection.on('reset', this.render, this);
console.log(this.collection);
},
render: function() {
return this.collection.each(this.addOne, this);
},
addOne: function(main) {
var mainC = new App.Views.Main({ model: main});
this.$el.append(mainC.render().el);
return this;
}
});
App.Views.Main = Backbone.View.extend({
tagName: 'li',
template: template('mainContactTemplate'),
render: function () {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
mains = new App.Collections.Mains();
main = new App.Views.Main({ collection: mains});
new App.Router;
Backbone.history.start();
})();
What I want to do is have the data returned in the ul to be bound to a DOM element called $('#web-leads'). How do I do that, given this code? Incidentally, I've already posted about this here and tried to follow the first answer and the second answer combined. But I still don't get HTML and data bound to the DOM. The data is returning from the server correctly in my collection, so I know that's not the problem. Don't worry about the router stuff. That's for later.
Generally in Backbone you don't put data on DOM elements: you put it in views that wrap that DOM element.
That being said, if you really want to store data on the element, jQuery has a function for that:
$('#web-leads').data('someKey', 'yourData');
Then you can retrieve that data with:
$('#web-leads').data('someKey');
* EDIT *
In a comments discussion with the OP it became apparent that the real goal was simply to append a view's element to an element on the page. If the element being appended to is #web-leads, then this can be accomplished with:
$('#web-leads').append(theView.render().el);
I have a backbone view that calls to a sub-view:
lr.MapView = Backbone.View.extend({
el: $('#map'),
foo: "bar",
initialize: function() {
var that = this;
_.bindAll(this, "render", "addAllEvents", "addOneEvent");
this.collection = new lr.Events();
this.collection.fetch({
success: function(resp) {
that.render();
that.addAllEvents();
}
});
},
addAllEvents: function() {
this.collection.each(this.addOneEvent);
},
addOneEvent: function(e) {
var ev = new lr.EventView({
model: e
});
},
render: function() {
}
});
Here is the sub-view:
lr.EventView = Backbone.View.extend({
initialize: function() {
_.bindAll(this, "render");
console.log(lr.MapView.foo); // will console.log 'undefined'
},
render: function() {
}
});
I'd like to be able to access properties the parent view within the sub-view, but it isn't working with the above code. For example, how can I access the 'foo' variable within the sub-view?
lr.MapView is a "class", everything that Backbone.View.extend builds will be in lr.MapView.prototype, not in lr.MapView. Run this with the console open and you'll see whats going on:
var MapView = Backbone.View.extend({ foo: 'bar' });
console.log(MapView);
console.log(MapView.prototype);
console.log(MapView.prototype.foo);
Demo: http://jsfiddle.net/ambiguous/DnvR5/
If you're only going to have a single MapView then you can refer to lr.MapView.prototype.foo everywhere:
initialize: function() {
_.bindAll(this, "render");
console.log(lr.MapView.prototype.foo);
}
Note that everywhere includes within lr.MapView instances so your foo will act like a "class variable" from non-prototype based OO languages.
The right way to do this is to use an instance variable for foo and pass the parent view instance to the sub-view instances when they're created:
// In MapView
addOneEvent: function(e) {
var ev = new lr.EventView({
model: e,
parent: this
});
}
// In EventView
initialize: function(options) {
_.bindAll(this, "render");
this.parent = options.parent; // Or use this.options.parent everywhere.
console.log(this.parent.foo);
}
Or better, add an accessor method to MapView:
_foo: 'bar',
foo: function() { return this._foo }
and use that method in EventView:
initialize: function(options) {
// ...
console.log(this.parent.foo());
}
Proper encapsulation and interfaces are a good idea even in JavaScript.
Just a guess, but could you try something like this in MapView:
addOneEvent: function(e) {
var that = this,
ev = new lr.EventView({
model: e,
parentView = that
});
}
And then access it like this:
lr.EventView = Backbone.View.extend({
initialize: function() {
_.bindAll(this, "render");
console.log(this.parentView.foo);
},
render: function() {
}
});
I have implemented a simple close() method for all the Backbone views which disposes of a view when its not needed/needs to be reset.
Backbone.View.prototype.close = function() {
if (this.onClose) {
this.onClose();
}
this.remove();
this.unbind();
};
NewView = Backbone.View.extend({
el: '#List ul',
initialize: function() {},
render: function() {
_(this.collection.models).each(function(item) {
this.renderChildren(item);
}, this);
},
renderChildren: function(item) {
var itemView = new NewChildView({ model: item });
$(this.el).prepend(itemView.render());
},
onClose: function() {
this.collection.reset();
// I want to remove the child views as well
}
});
NewChildView = Backbone.View.extend({
tagName: 'li',
render: function() {
}
});
Now, when I remove the parent view, I also want to remove all the child views here. Any ideas how can I can do this without looping through the models like this....
_(this.collection.models).each(function(item) {
item.close();
}, this);
I think in most of the cases you should keep the view removal in the view layer, without affecting your models.
For example, if you remove a view with comments, maybe another view in your app shows a selection of comments, or some statistics, and resetting the collection would affect those views too.
So I think you should keep it all in the view (only relevant methods included):
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();
});
}
});
I have got a huge list of tasks loaded on the start.
I want to show them depending on selected list / inbox, so that there won't be additional loadings for each list.
window.Task = Backbone.Model.extend({});
window.TasksCollection = Backbone.Collection.extend({
model: Task,
url: '/api/tasks',
inbox: function() {
return this.filter(function(task) {
return task.get('list') == null;
});
},
list: function(id) {
return this.filter(function(task) {
return task.get('list') == id;
});
}
});
window.tasks = new TasksCollection;
window.TaskView = Backbone.View.extend({
tagName: 'li',
template: _.template($('#item-template').html()),
initialize: function() {
_.bindAll(this, 'render', 'close');
this.model.bind('change', this.render);
this.model.view = this;
},
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
this.setContent();
return this;
},
});
window.TasksView = Backbone.View.extend({
el: '#todo-list',
collection: tasks,
initialize: function() {
_.bindAll(this, 'render');
this.collection.bind('reset', this.render);
this.collection.fetch();
},
render: function() {
var t = this;
$(t.el).html('');
this.collection.each(function(task) {
var view = new TaskView({ model:task });
$(t.el).append( view.render().el );
});
return this;
},
});
window.Nicetask = Backbone.Router.extend({
routes: {
'': 'inbox',
'/inbox': 'inbox',
'/list/:id': 'list',
},
initialize: function() {
_.bindAll(this, 'inbox', 'list');
window.tasksView = new TasksView;
},
inbox: function() {
tasks.reset( tasks.inbox() );
},
list: function(id) {
tasks.reset( tasks.list(id) );
}
});
This code works, but the reset() function removes other tasks in actual list from tasks collection. And on another route, tasks collection is empty.
Is there any reasonable way to achieve this? thanks for any idea.
ps: backbone novice
UPDATE
Thx to #sled and #ibjhb for comments, here is snippet of working solution.
window.TasksView = Backbone.View.extend({
el: '#todo-list',
collection: Backbone.Collection.extend(),
initialize: function() {
_.bindAll(this, 'render', 'addOne', 'addAll');
this.collection.bind('add', this.addOne);
this.collection.bind('reset', this.render);
},
render: function(data) {
$(this.el).html('');
_.each(data, function(task) {
this.addOne(task);
}, this);
return this;
},
addOne: function(task) {
var view = new TaskView({ model:task });
$(this.el).append( view.render().el );
},
});
window.Nicetask = Backbone.Router.extend({
routes: {
'': 'inbox',
'/inbox': 'inbox',
'/today': 'today',
'/list/:id': 'list',
},
initialize: function() {
_.bindAll(this, 'inbox', 'today');
window.tasksView = new TasksView;
window.menuView = new MenuListView;
tasks.fetch();
},
inbox: function() {
tasksView.render( tasks.inbox() );
},
today: function() {
tasksView.render( tasks.today() );
},
list: function(id) {
tasksView.render( tasks.list(id) );
}
});
I think you need to use another collection. For example, in your inbox, do this:
inbox: function(){
currentCollection = new TasksCollection(tasks.inbox());
}
I haven't tested this but when you do a .reset(); you are removing all your models and loading the ones passed in.
#sled there's typos in the code you posted, see comments inline. Did you post this as a project somewhere?
// add models
add: function(models, options) {
// TYPO: next line was missing, so single models not handled.
models = _.isArray(models) ? models.slice() : [models];
var self = this;
models = _.filter(models, this.filter);
// return if no models exist
// TYPO: returned undefined, so was not chainable
if(models.length == 0) { return this; }
// actually add the models to the superset
this.superset.add(models, options);
return this;
},
// remove models
remove: function(models, options) {
// TYPO: next line was missing, so single models not handled.
models = _.isArray(models) ? models.slice() : [models];
// remove model from superset
this.superset.remove(_.filter(_.filter(models, function(cm) {
// TYPO: not 'm != null', causes error to be thrown
return cm != null;
}), this.filter), options);
// TYPO: missing return so not chainable
return this;
},
one quick amendment to you solution, you are using
$(this.el).html('');
My understanding is your the views and related event bindings will still exist in the browser memory, so you ideally need to use view.remove() on the TaskView to correctly clear the event bindings as well as the html.
This is a slightly different take on the answer as I have been looking for a solution to a similar problem, hope this may be of help to others.
My problem: - to filter a complete collection by attributes of the model. eg. a user clicks the models view, gets a list of (some of) the attributes, selecting an attribute filters the collection to only show ones with the same value.
The route I am taking is by calling a method on the collection from the view, in my case the view is specific to a model so:
this.model.collection.myFilter(attr,val);
where attr is an attribute of the model associated with the collection, then in the filter something like
myFilter: function(attr, val){
var groupByAttr = this.groupBy(function(article){
var res = (val === undefined)? true : (article.get(attr) == val);
article.set({selected:res});
return res;
});
return groupByAttr;
}
I have used ._groupBy as this returns 2 arrays (positive / negative) that may be of use. By setting the mode attribute "selected", and binding to this in the model view I can easily toggle a class which shows or hides the view.
if(val === undefined) is added as a simple way of clearing a filter by calling the same method without a value.