I'm creating a Backbone app with a section to view reports; the section has three parts: a menu of report links, the title of the displayed report, and the content of the displayed report. The user is to click on a report link, which will fetch the associated model's data. Then the report title and content should update accordingly. However, I'm not sure how the view bindings should work, and each report may return slightly different data that requires a different view template. Here's my JSFiddle (fetch method overridden just for this example)
Right now, I have a Backbone model for each report and a Backbone collection of all the reports:
App.Models.Report = Backbone.Model.extend();
App.Collections.Reports = Backbone.Collection.extend({
model: App.Models.Report,
url: "/reports"
});
The menu view is tied to the collection and on click, sets App.State.title and App.State.cid, which the other two views are listening to:
App.Views.ReportLink = Backbone.View.extend({
tagName: 'li',
className: 'is-clickable',
initialize: function() {
this.render();
},
render: function() {
this.el.innerHTML = this.model.get('title');
this.$el.attr('data-CID', this.model.cid); // store the model's cid
}
});
App.Views.ReportMenu = Backbone.View.extend({
tagName: 'ul',
initialize: function() {
this.listenTo(this.collection, 'reset', this.render)
this.render();
this.$el.on('click', 'li', function() {
App.State.set({
'title': this.innerHTML,
'cid': $(this).attr('data-CID') // cid of the clicked view's model
});
});
},
The difficulty is with the report content; what it currently does is listen for changes to App.State.cid and then calls fetch on the given model with that cid. This fetch populates the model with a sub-collection of report rows. The report content view then set its html based on the sub-collection data, and it is also supposed to apply the correct template to the data:
App.Views.ReportContent = Backbone.View.extend({
initialize: function(attrs) {
this.listenTo(this.model, 'change:cid', this.render);
this.reportsCollection = attrs.reportsCollection;
},
render: function() {
var self = this,
cid = this.model.get('cid'),
model = this.reportsCollection.get(cid);
model.fetch({
success: function() {
var html = '';
model.subCollection.each(function(model) {
var template = _.template($('#templateReportA').html()); // want to dynamically set this
html += template(model.toJSON());
});
self.$el.html(html);
}
});
}
});
1) Is this the correct sort of implementation for this type of multiple view situation with a collection?
2) How can I pass the correct template that needs to apply for each individual report? Right now I'm explicitly passing the view template for report A. I can think of storing it on the model but the template should be associated with the view.
If your cids are all made up of characters that are legal in HTML ids, then a simple solution would be to name all your report templates templateReportxxx where "xxx" is the report's cid and then just change the template-loading line to
var template = _.template($('#templateReport'+cid).html());
Related
I'm struggling with a design decision and looking for some feedback. I don't think this question is necessarily specific to Backbone, but that's the framework I'm currently using.
I'm wondering if it's considered bad practice to store the classname of a view as part of a model. For example, let's say you have a parent view with multiple subviews of different types. Each subview contains an edit link, and when that edit link is clicked, the parent view should update it's contents to contain the edit view for that model. I'm using an "event bus" to orchestrate events.
For example:
var E = _.extend({}, Backbone.Events);
var ParentView = Backbone.View.extend({
initialize: function(options) {
this.apples = options.apples; // Backbone Collection of Apple models
this.oranges = options.oranges; // Backbone Collection of Orange models
this.$appleList = this.$('#apples');
this.$orangeList = this.$('#oranges');
this.$editScreen = this.$('#edit-screen');
// listen to edit events for models and render the
// edit screen for using the appropriate view
this.listenTo(E, 'edit', this.showEditScreen);
},
template: 'templates/parent',
render: function() {
this.$el.html(this.model.toJSON());
this.renderAppleViews();
this.renderOrangeViews();
},
renderAppleViews: function() {
var view = new AppleListView({collection: this.apples});
this.$appleList.html(view.render().el);
},
renderOrangeViews: function() {
var view = new OrangeListView({collection: this.oranges});
this.$orangeList.html(view.render().el);
},
// Show the edit screen for a particular model
showEditScreen: function(model) {
var view = new window[model.editScreenViewClass]({model: model}):
this.$editScreen.html(view.render().el);
}
});
AppleListView and OrangeListView simply loop through their respective collections and append a view to the list.
AppleListView adds AppleItemViews, and OrangeListView adds OrangeItemViews. I'm showing the relevant parts of those views below:
var AppleItemView = Backbone.View.extend({
events: {
'click .edit': 'onEditClick'
},
onEditClick: function(e) {
e.preventDefault();
E.trigger('edit', this.model);
}
});
var OrangeItemView = Backbone.View.extend({
events: {
'click .edit': 'onEditClick'
},
onEditClick: function(e) {
e.preventDefault();
E.trigger('edit', this.model);
}
});
Here is what the models would look like for this to work:
var Apple = Backbone.Model.extend({
editScreenViewClass: 'AppleEditView'
});
var Orange = Backbone.Model.extend({
editScreenViewClass: 'OrangeEditView'
});
I'm asking if it seems "ok" to store this editScreenViewClass on the model. That way I can retrieve it directly from the model passed into the event.
I am trying to place the rendered output of a Collection View onto the dom. However, only the last object in the collection is displayed on the page at the end of the process.
I have a event handler set up on the view so that when an item is clicked, it's title is logged out. Whenever I click on this single element that is placed onto the Dom, the title for each of my objects is logged, even though only one is displayed, so each handler is being applied to the final element but is somehow logging out the correct titles.
Does anybody know how I can render out each item in the collection rather than just the final one? Below is a quick tour through my code.
The end goal is to list out the name of each film.
Model
First, define the model - nothing exciting here
var FilmModel = Backbone.Model.extend({});
View
Here is a simplified version of the View I have made for the Film model
var FilmView = Backbone.View.extend({
// tagName: 'li',
initialize: function() {
this.$el = $('#filmContainer');
},
events: {
'click': 'alertName'
},
alertName: function(){
console.log("User click on "+this.model.get('title'));
},
template: _.template( $('#filmTemplate').html() ),
render: function(){
this.$el.html( this.template( this.model.attributes ) );
return this;
}
});
Collection
Again, seems standard.
var FilmList = Backbone.Collection.extend({
model: FilmModel,
});
Collection View
Adapted from a Codeschool course I took on Backbone
var FilmListView = Backbone.View.extend({
// tagName: 'ul',
render: function(){
this.addAll();
return this;
},
addAll: function(){
this.$el.empty();
this.collection.forEach(this.addOne, this);
},
addOne: function(film){
var filmView = new FilmView( { model: film } );
this.$el.append(filmView.render().el);
// console.log(this.$el);
}
});
Go time
var filmList = new FilmList({});
var filmListView = new FilmListView({
collection: filmList
});
var testFilms = [
{title: "Neverending Story"},
{title: "Toy Story 2"}
];
filmList.reset(testFilms);
filmListView.render();
From my understanding of Backbone so far, what this should be doing is appending, using the template specified in FilmView to render each item in the filmList collection into the el in the filmListView.
However, what actually happens is that the final title is always placed on the DOM.
I initially (when this was pulling in from an API) thought that the issue might be similar to this question, however now that I am resetting with my own testFilms, I can be positive that I am not overriding or setting any id attribute that I shouldn't.
Does anybody have any ideas?
I think it could be that you set the el of FilmView to an id, which should always be unique, however then you loop over the collection and continually reset that el/id with the current model since each FilmView is going to have the same el
I am trying to understand backbone and am currently struggling with zombie views. I have read many stack overflow posts on the matter but I still cannot figure it out.
For the sake of simplicity, I set up two views (without data) that I need to switch.
What I did so far was:
creating an object
//define application object
var app = {
vent: {},
templates: {},
views: {},
routers: {},
};
//instantiate event aggregator and attach it to app
app.vent = _.extend({}, Backbone.Events);
defining two very simple templates (stored into app.templates): the first one has some dummy text and a button (with and id of 'test-begin'), the second one just dummy text
defining two views
app.views.instructions = Backbone.View.extend({
//load underscore template
template: _.template(app.templates.instructions),
//automatically called upon instantiation
initialize: function(options) {
//bind relevant fucntions to the view
_.bindAll(this, 'render', 'testBegin', 'stillAlive', 'beforeClose');
//listen to app.vent event
this.listenTo(app.vent, 'still:alive', this.stillAlive);
},
//bind events to DOM elements
events: {
'click #test-begin' : 'testBegin',
},
//render view
render: function() {
this.$el.html(this.template());
return this;
},
//begin test
testBegin: function() {
Backbone.history.navigate('begin', {trigger: true});
},
//still alive
stillAlive: function() {
console.log('I am still alive');
},
//before closing
beforeClose: function() {
//stop listening to app.vent
this.stopListening(app.vent);
},
});
//test view
app.views.test = Backbone.View.extend({
//load underscore template
template: _.template(app.templates.test),
//automatically called upon instantiation
initialize: function(options) {
//trigger still:alive and see if removed view responds to it
app.vent.trigger('still:alive');
//bind relevant fucntions to the view
_.bindAll(this, 'render');
},
//render view
render: function() {
this.$el.html(this.template());
return this;
},
});
defining a router
//base router
app.routers.baseRouter = Backbone.Router.extend({
//routes
routes: {
'': "instructions",
'begin': "beginTest"
},
//functions (belong to object controller)
instructions: function() {baseController.instructions()},
beginTest : function() {baseController.beginTest()},
});
//baseRouter controller
var baseController = {
instructions: function() {
mainApp.viewsManager.rederView(new app.views.instructions());
},
beginTest: function(options) {
mainApp.viewsManager.rederView(new app.views.test());
},
};
defining mainApp (with a view-switcher)
//define mainApplication object
mainApp = {};
//manages views switching
mainApp.viewsManager = {
//rootEl
rootEl: '#test-container',
//close current view and show next one
rederView : function(view, rootEl) {
//if DOM el isn't passed, set it to the default RootEl
rootEl = rootEl || this.rootEl;
//close current view
if (this.currentView) this.currentView.close();
//store reference to next view
this.currentView = view;
//render next view
$(rootEl).html(this.currentView.render().el);
},
};
//render first view of app
mainApp.viewsManager.rederView(new app.views.instructions());
//initiate router and attach it to app
mainApp.baseRouter = new app.routers.baseRouter();
//start Backbone history
Backbone.history.start({silent: true
});
adding a close function to view via Backbone prototype
//add function to Backbone view prototype (available in all views)
Backbone.View.prototype.close = function () {
//call view beforeClose function if it is defined in the view
if (this.beforeClose) this.beforeClose();
//this.el is removed from the DOM & DOM element's events are cleaned up
this.remove();
//unbind any model and collection events that the view is bound to
this.stopListening();
//check whether view has subviews
if (this.hasOwnProperty('_subViews')) {
//loop thorugh current view's subviews
_(this._subViews).each(function(child){
//invoke subview's close method
child.close();
});
}
};
So, in order to check for zombie views, the second view triggers and event (still:alive) that the first view listen to and respond to it via a message sent to the console.log (although it really shouldn't).
The first view does listen to such a message (in the console log I read 'I am still alive) even when it has been replaced by the second view.
Can you help me? thank you very.
Long post, if you have any questions, please ask
A Zombie View is just a view that is not in the DOM, but listens to and reacts to events -- sometimes this behavior is expected, but not typically.
If the DOM Event handlers for the view are not properly removed, the view and it's in-memory HTML fragments will not be garbage collected. If the Backbone.Event handlers are not unbound properly, you could have all sorts of bad behavior... such as a bunch of "Zombie" view triggering AJAX requests on models. This problem was very common on older versions of Backbone prior to stopListening and listenTo especially if you shared models between views.
In your code, you don't have a Zombie View, because you are properly closing your views.
You can see the console.log because you are initializing the second view (and triggering the event still:alive) before you close the first view.
To switch views, you are calling:
mainApp.viewsManager.rederView(new app.views.test());
Calling new app.views.test() initializes the second view which triggers the event that the first listens to.
If you update your code to the following, you won't see the console.log anymore.
//baseRouter controller
var baseController = {
instructions: function() {
mainApp.viewsManager.rederView(app.views.instructions);
},
beginTest: function(options) {
mainApp.viewsManager.rederView(app.views.test);
},
};
And update rederView
rederView : function(ViewClass, rootEl) {
//if DOM el isn't passed, set it to the default RootEl
rootEl = rootEl || this.rootEl;
//close current view
if (this.currentView) this.currentView.close();
//store reference to next view
this.currentView = new ViewClass();
//render next view
$(rootEl).html(this.currentView.render().el);
},
If you remove this line from your close method, you will have a zombie view and should see the console.log.
//unbind any model and collection events that the view is bound to
this.stopListening();
Zombie View Example
In the following code, I am creating 100 views, but only displaying 1 in the DOM. Every view contains the same model and listens to it's change event. When the view's <button> element is clicked, it updates the model which causes every view's model change handler to be executed, calling fetch 100 times... 100 AJAX requests!
The view's change handlers are called 100 times, because the view close method does not call this.stopListening(), so even when the views are removed from the page, they all still listen to the model's events. Once you click the button, the model is changed, and all of the zombie views respond, even though they're not on the page.
var TestView = Backbone.View.extend({
tagName: 'h1',
initialize: function(options) {
this.i = options.i;
this.listenTo(options.model, 'change', function(model) {
model.fetch();
});
},
events: {
'click button': function() {
this.model.set("show_zombies", Date.now());
}
},
render: function() {
this.$el.append("<button>Click To Test for Zombies!</button>");
return this;
},
close: function() {
this.$el.empty(); // empty view html
// this.$el.off(); // // Whoops! Forgot to unbind Event listeners! (this view won't get garbage collected)
// this.stopListening() // Whoops! Forgot to unbind Backbone.Event listeners.
}
});
var model = new (Backbone.Model.extend({
fetch: function() {
document.body.innerHTML += "MODEL.FETCH CALLED<br />"
}
}));
var v;
for (var i = 1; i < 101; i++) {
if (v) v.close();
v = new TestView({
'i': i,
'model': model
}).render();
$('body').html(v.el);
}
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.2/backbone.js"></script>
Being new to Backbone.js, I try to develop a SPA following, amongst others, Addy Osmani's "Developing Backbone.js Applications". It's exercise 2 (http://addyosmani.github.io/backbone-fundamentals/#exercise-2-book-library---your-first-restful-backbone.js-app) shows, how to use a collection view to render inner model views from each of the collections objects. However, the collection view in this example does not come with its own html markup. Thus, the collection's models are associated with the collection view's DOM element (here: '#books'). I want to use an own template to first render the html elements of my collection view, say, a simple div with id="the-plan". Problem is, "#the.plan" is not recognized from the inner model views as element attribute. Hence, the inner views are not rendered at all. There is no error message and all console.logs are working. Code looks something like this:
app.PlanItemView = Backbone.View.extend({
className: "plan-item",
template: _.template($("#plan-item-view-template").html()),
render: function(){
console.log("Rendering plan item view...");
this.$el.append(this.template(this.model.toJSON()));
return this;
}
});
app.PlanView = Backbone.View.extend({
el: ".main-panel",
id: "#the-plan",
template: _.template($("#plan-view-template").html()),
initialize: function(initialPlanItems){
console.log("Plan View initialized... Selector: " + this.id);
console.log("Incoming initial plan item data: " + _.first(_.values(_.first(initialPlanItems))));
this.collection = new app.MealPlan(initialPlanItems);
this.render();
},
// render plan by rendering each item in its collection
render: function() {
this.$el.append(this.template({
"myPlan": this.collection.each(function(item){
this.renderPlanItem(item);
}, this)
}));
return this;
},
// render a plan item by creating a PlanItemView and appending the
// element it renders to the plan's id-element ('#the-plan')
renderDish: function(item){
var planItemView = new app.PlanItemView({
model: item,
el: this.id
});
this.$("#the-plan").append(planItemView.render());
}
});
...
var planView = new app.PlanView(test_plan_items);
What's wrong here?
Change the render function to :
render: function() {
this.$el.append(this.template({
"myPlan": this.collection
}));
this.collection.each(function(item){
this.renderPlanItem(item);
}, this);
return this;
}
And change the renderDish to :
renderPlanItem: function(item){
var planItemView = new app.PlanItemView({
model: item,
el: this.id
});
planItemView.render();
}
Here is my content.js in which i am using backbone.js for rendering contents.
// Our basic **Content** model has `content`, `order`, and `done` attributes.
var Content = Backbone.Model.extend({
// If you don't provide a Content, one will be provided for you.
EMPTY: "empty Content...",
// Ensure that each Content created has `content`.
initialize: function() {
}
});
var ContentCollection = Backbone.Collection.extend({
model : Content
});
// Create our global collection of **Todos**.
window.Contents = new ContentCollection;
// The DOM element for a Content item...
var ContentView = Backbone.View.extend({
//... is a list tag.
tagName: "li",
events: {
"click .content": "open"
},
// a one-to-one correspondence between a **Content** and a **ContentView** in this
// app, we set a direct reference on the model for convenience.
initialize: function() {
_.bindAll(this, 'render', 'close');
this.model.bind('change', this.render);
this.model.view = this;
},
// Re-render the contents of the Content item.
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
Here is how i am making the list of content and rendering them.
for(var i=0; i<data.length; i++) {
var content = new Content(data[i]);
var templ=_.template($('#tmpl_content').html());
var view = new ContentView({model: content});
view.template=templ;
$("#content").append(view.render().el);
}
my question is how can i get the contetnt model listing .
as i have created the collection
var ContentCollection = Backbone.Collection.extend({
model : Content
});
// Create our global collection of **Todos**.
window.Contents = new ContentCollection;
So when i do watch Contents it shows length 0 and models [] .
how contetnt will get added in the collection . or how to see list of model in backbone.js
You need to Collection.add(models) before it will contain anything.
You also could specify a URL (which should return a JSON array of models) on your collection and then do window.Contents.fetch(). Backbone will auto-populate the model (Content) specified in your collection and automatically add them to your collection.