Rendering Handlebars template with Backbone - backbone.js

I have a Backbone view (see below) that I believe to be doing the right thing
Index = Backbone.View.extend({
render: function() {
var activities = new Activities();
activities.fetch();
var tpl = Handlebars.compile($("#activities-template").html());
$(this.el).html(tpl({activities: activities.toJSON()}));
return this;
}
});
If execute each line in the render() function with Chrome JS console I get the expected result with the element I pass in getting populated with the template output. However, when I run this using the following
var i = new Index({el: $("body")})
i.render()
"i.$el" is completely empty--the HTML is not getting rendered like it does in console. Any ideas why?

fetch is an AJAX call so there's no guarantee that activities.toJSON() will give you any data when you do this:
activities.fetch();
var tpl = Handlebars.compile($("#activities-template").html());
$(this.el).html(tpl({activities: activities.toJSON()}));
Executing the code in the console probably gives the AJAX call time to return with something before you try to use activities.
You should do two things:
Fix your template to do something sensible (such as show a loading... message of some sort) if activities is empty.
Attach your view's render to the collection's "reset" event:
initialize: function() {
// Or, more commonly, create the collection outside the view
// and say `new View({ collection: ... })`
this.collection = new Activities();
this.collection.on('reset', this.render, this);
this.collection.fetch();
},
render: function() {
var tpl = Handlebars.compile($("#activities-template").html());
this.$el.html(tpl({activities: this.collection.toJSON()}));
return this;
}
I also switched to this.$el, there's no need to $(this.el) when Backbone already gives you this.$el.

Related

Add to backbone collection in response to event

I'm getting started with backbone.js but have got stuck at this point: I want to get data from a websocket and add it to a collection.
I've started with the code from http://adrianmejia.com/blog/2012/09/11/backbone-dot-js-for-absolute-beginners-getting-started/ but when I try to add in the event handling I cant figure out how to access the collection from within the event.
var AppView = Backbone.View.extend({
// el - stands for element. Every view has a element associate in with HTML content will be rendered.
el: '#container',
template: _.template("<h3>Hello <%= who %></h3>"),
// It's the first function called when this view it's instantiated.
initialize: function(){
console.log(this.collection) //<-- this.collection is OK here
MyPubSub.on('message', function (evt) {
this.collection.add(evt.data) //<-- TypeError: this.collection is undefined
});
this.render();
},
// $el - it's a cached jQuery object (el), in which you can use jQuery functions to push content. Like the Hello World in this case.
render: function(){
this.$el.html(this.template({who: 'planet!'}));
}
});
function init()
{
var websocket = new WebSocket("ws://example.com:8088");
MyPubSub = $.extend({}, Backbone.Events);
websocket.onmessage = function(evt) {
console.log("message")
MyPubSub.trigger('message',evt)
};
var appView = new AppView({collection:new AlertCollection()});
}
window.addEventListener("load", init, false);
I'm a complete newbie at this so I suspect I've made some kind of basic error in scoping. Also open to other approaches to read a websocket steam into a backbone app.
In the initialize function, add var myCollection = this.collection; and use myCollection within the MyPubSub.on(....

Bug while creating object in View

I'm working on a backbone.js project which is mainly to learn backbone framework itself.
However I'm stuck at this problem which i can't figure out but might have an idea about the problem...
I've got an Create View looking like this...
define(['backbone', 'underscore', 'jade!templates/addAccount', 'models/accountmodel', 'common/serializeObject'],
function(Backbone, underscore, template, AccountModel, SerializeObject){
return Backbone.View.extend({
//Templates
template: template,
//Constructor
initialize: function(){
this.accCollection = this.options.accCollection;
},
//Events
events: {
'submit .add-account-form': 'saveAccount'
},
//Event functions
saveAccount: function(ev){
ev.preventDefault();
//Using common/serializeObject function to get a JSON data object from form
var myObj = $(ev.currentTarget).serializeObject();
console.log("Saving!");
this.accCollection.create(new AccountModel(myObj), {
success: function(){
myObj = null;
this.close();
Backbone.history.navigate('accounts', {trigger:true});
},
error: function(){
//show 500?
}
});
},
//Display functions
render: function(){
$('.currentPage').html("<h3>Accounts <span class='glyphicon glyphicon-chevron-right'> </span> New Account</h3>");
//Render it in jade template
this.$el.html(this.template());
return this;
}
});
});
The problem is that for every single time I visit the create page and go to another and visit it again. It remebers it, it seems. And when i finally create a new account I get that many times I've visited total number of accounts...
So console.log("Saving!"); in saveAccount function is called x times visited page...
Do I have to close/delete current view when leaving it or what is this?
EDIT
Here's a part of the route where i init my view..
"account/new" : function(){
var accCollection = new AccountCollection();
this.nav(new CreateAccountView({el:'.content', accCollection:accCollection}));
console.log("new account");
},
/Regards
You have zombie views. Every time you do this:
new CreateAccountView({el:'.content', accCollection:accCollection})
you're attaching an event listener to .content but nothing seems to be detaching it. The usual approach is to call remove on a view to remove it from the DOM and tell it to clean up after itself. The default remove does things you don't want it to:
remove: function() {
this.$el.remove();
this.stopListening();
return this;
}
You don't want that this.$el.remove() call since your view is not responsible for creating its own el, you probably want:
remove: function() {
this.$el.empty(); // Remove the content we added.
this.undelegateEvents(); // Unbind your event handler.
this.stopListening();
return this;
}
Then your router can keep track of the currently open view and remove it before throwing up another one with things like this:
if(this.currentView)
this.currentView.remove();
this.currentView = new CreateAccountView({ ... });
this.nav(this.currentView);
While I'm here, your code will break as soon as you upgrade your Backbone. As of version 1.1:
Backbone Views no longer automatically attach options passed to the constructor as this.options, but you can do it yourself if you prefer.
So your initialize:
initialize: function(){
this.accCollection = this.options.accCollection;
},
won't work in 1.1+. However, some options are automatically copied:
constructor / initialize new View([options])
There are several special options that, if passed, will be attached directly to the view: model, collection, el, id, className, tagName, attributes and events.
so you could toss out your initialize, refer to this.collection instead of this.accCollection inside the view, and instantiate the view using:
new CreateAccountView({el: '.content', collection: accCollection})

How to properly update subviews on Marionette when using constantly updating JSON

I have a single page app built with Marionette that has a main view with a list of subviews.
The JSON which holds all application data is updated constantly. I've tried to separate region show code so that it will be run just once and not on every render.
Now the render event is fired on every timeout loop even though the JSON is static data and therefore change event should not call render. What is wrong? I assume it has something to do with .set but is there any other way to load the response from an array variable to the subview collection, since fetch will allow only url attribute and will not accept array variable?
This example is an extremely simplified version of the application code to concentrate on this specific problem.
Controller:
var Controller = {
showMainView: function(id){
// create model and fetch data on startup
var mainElement = new mainElement();
var mainElementFetched = mainElement.fetch({url: 'http://json.json'});
// fetch done, create view, show view in region, setTimeout
mainElementFetched.done(function(data){
var mainElementView = mainElementView({model:mainElement});
App.mainRegion.show(mainElementView);
setTimeout(updateJSON, 5000);
}
// timeOut loop to check for JSON changes
var updateJSON = function(){
mainElement.fetch({url: 'http://json.json'});
App.timeOut = setTimeout(updateJSON, 5000);
}
}
}
MainElement Model:
MainElement = Backbone.Model.extend({
parse : function(response){
// parsing code
return response;
}
});
MainElementView (Layout):
MainElementView = Backbone.Marionette.Layout.extend({
template: "#main-template",
initialize:function(){
//create collection for subelements
this.subElementCollection = new SubElementCollection();
//listen to change event, and fire callback only when change in model is detected
this.model.on('change', this.render, this);
},
regions:{
subsection : ".subsection"
},
onShow: function(){
// show subelements in subsection region when mainelementview is shown on screen, but not show on every render
this.subsection.show(new SubElementCompositeView({collection:this.subElementCollection}))
},
onRender : function(){
var response = this.model.response;
// get subelements when change event fires and parse the response
this.subElementCollection.set(response,{parse:true});
}
});
SubElement Model, Collection, ItemView, CompositeView:
SubElement = Backbone.Model.extend({});
SubElementCollection = Backbone.Collection.extend({
model:SubElement,
comparator : function(model){
var price = model.get('price');
return -(price);
},
parse:function(response){
// parsing code to get data to models from differents parts of JSON
return response;
}
});
SubElementItemView = Backbone.Marionette.ItemView.extend({
template: "#subelement-template",
tagName: "tr"
});
SubElementCompositeView = Backbone.Marionette.CompositeView.extend({
template: "#subelements-template",
tagName : "table",
itemView:SubElementItemView,
itemViewContainer : "tbody",
initialize: function(){
this.collection.on('change', this.render, this);
},
appendHtml : function(collectionView,itemView,index){
// SORTING CODE
},
onRender:function(collectionView,itemView,index){
// ADD IRRELEVANT EXTERNAL STUFF TO TEMPLATE AFTER RENDER
}
});
Check out the documentation. It says:
A "change" event will be triggered if the server's state differs from the current attributes.
In your MainElementView where you bind to your model's change event: this.model.on('change', this.render, this);, you are actually saying every time your model changes, call render.
If re-drawing the whole view is too slow, due to the amount of changes. Why don't you make your rendering more fine-grained? For example you could listen for specific change events and just change the DOM elements which need changing:
this.model.on('change:Name', function () {
this.$('[name=Name]').html(this.model.get('Name'));
}, this);
It is more work to set this up, but you could make it a bit cleverer by matching the model property names to your DOM element name or something.

Backbone append models works in console but not after $(document).ready

My ultimate goal is to append the JSON data to ul#tweets, each as individual hidden list items. They will then, one by one over time, become visible/shown on the screen, and then be removed from the ul#tweets list.
Once the number of hidden items drops below a certain amount, I want to re-append the JSON data. When this happens, I am not worried about duplicate items.
I tried to setup a test by creating a function with a timeout so that every 5 seconds it would append the JSON data to the list.
However, though my app loads the initial data on pageload fine, when I create a function to be run within $(document).ready({}) - it won't work.
I do know, however, that I can append the JSON data manually in the console after page load (same code as below without wrapping it in the function or the doc.ready).
Thanks for the help!
Function:
$(document).ready(function(){
updateTweets = function() {
newTweets = new Tweets();
newTweets.fetch();
newTweets.each( function(tweet) {
console.log('test'); // this doesn't work
view = new TweetView({ model:tweet });
$('#tweets').append(view.render().el);
});
setTimeout(updateTweets, 5000);
};
updateTweets();
});
Here is my Code
// MODEL
window.Tweet = Backbone.Model.extend({});
// COLLECTION
window.Tweets = Backbone.Collection.extend({ model: Tweet, url: '/tweets' });
// SET GLOBAL VARIABLE FOR NEW TWEETS COLLECTION
window.tweetList = new Tweets();
$(document).ready(function() {
// MODEL VIEW
window.TweetView = Backbone.View.extend({
tagName: 'li',
className: 'tweet',
initialize: function() {
_.bindAll(this, 'render');
this.model.bind('change', this.render);
this.template = _.template($('#tweet-template').html());
},
render: function(){
var renderedTweets = this.template(this.model.toJSON());
$(this.el).html(renderedTweets);
return this;
}
});
// COLLECTION VIEW
window.TweetListView = Backbone.View.extend({
template: _.template($('#tweet-list-template').html()),
initialize: function() {
_.bindAll(this, 'render');
this.collection.bind('reset', this.render);
},
render: function() {
var $tweets,
collection = this.collection;
$(this.el).html(this.template({}));
$tweets = this.$('#tweets');
collection.each(function(tweet){
var view = new TweetView({
model: tweet,
collection: collection
});
$tweets.append(view.render().el);
});
return this;
}
});
// ROUTER
window.TweetListDisplay = Backbone.Router.extend({
routes: {
'': 'home'
},
initialize: function(){
this.tweetListView = new TweetListView({
collection: window.tweetList
});
},
home: function() {
var $container = $('#container');
$container.empty();
$container.append(this.tweetListView.render().el);
},
});
// DECLARE AND START APP
window.app = new TweetListDisplay();
Backbone.history.start();
}); // close $(document).ready({});
You call fetch here
newTweets.fetch();
And then right after start processing the collection as if it has been populated, here
newTweets.each( function(tweet) {
console.log('test'); // this doesn't work
view = new TweetView({ model:tweet });
$('#tweets').append(view.render().el);
});
fetch is an ASYNCHRONOUS operation, which means that after you fire it, the rest of the program will continue to execute immediately after, regardless if the ajax-call launched by the fetch has returned or not. So when you start processing the collection, your fetch hasn't yet returned and the collection is still empty.
There are 2 ways you can correct this situation. Let's start by making a function processCollection that does to the collection exactly what you want:
var processCollection = function () {
newTweets.each( function(tweet) {
console.log('test'); // this doesn't work
view = new TweetView({ model:tweet });
$('#tweets').append(view.render().el);
});
};
1 The callback function (I don't like these)
newTweets.fetch(success: processCollection);
Now processCollection will be called right after the fetch has succeeded.
2 Bind to events (I prefer this)
newTweets.on('reset', processCollection);
newTweets.fetch();
When the fetch returns successfully, it will populate the collection and fire a reset -event. This is a good place to tie your processing event, because you know that now the collection is populated. Also I find that there is slightly less scoping problems with events than with callbacks.
Hope this helps!
You cant call;
newTweets.fetch();
And then immediately start processing the collection as if its ready to use.. it takes time.. the fetch call is asynchronous.. the reason it works in console is that it takes time to prep the output for console.. and the fetch does indeed finish..
You should provide a success callback for the fetch like this:
newTweets.fetch({success: function(){//process collection}});

Backbone.js: retrieving a collection from the server in PHP

I'm having a look at Backbone.js, but I'm stuck. The code until now is as simple as is possible, but I seem not to get it. I use Firebug and this.moments in the render of MomentsView is an object, but all the methods from a collection don't work (ie this.moments.get(1) doesn't work).
The code:
var Moment = Backbone.Model.extend({
});
var Moments = Backbone.Collection.extend({
model: Moment,
url: 'moments',
initialize: function() {
this.fetch();
}
});
var MomentsView = Backbone.View.extend({
el: $('body'),
initialize: function() {
_.bindAll(this, 'render');
this.moments = new Moments();
},
render: function() {
_.each(this.moments, function(moment) {
console.log(moment.get('id'));
});
return this;
}
})
var momentsview = new MomentsView();
momentsview.render();
The (dummy) response from te server:
[{"id":"1","title":"this is the moment","description":"another descr","day":"12"},{"id":"2","title":"this is the mament","description":"onother dascr","day":"14"}]
The object has two models according to the DOM in Firebug, but the methods do not work. Does anybode have an idea how to get the collection to work in the view?
The problem here is that you're fetching the data asynchronously when you initialize the MomentsView view, but you're calling momentsview.render() synchronously, right away. The data you're expecting hasn't come back from the server yet, so you'll run into problems. I believe this will work if you call render in a callback to be executed once fetch() is complete.
Also, I don't think you can call _.each(this.moments) - to iterate over a collection, use this.moments.each().
Try removing the '()' when instantiate the collection.
this.moments = new Moments;
Also, as it's an asynchronous call, bind the collection's 'change' event with the rendering.
I hope it helps you.

Resources