I'm building a website with the Javascript Backbone framework and the standard underscore templating engine. I've got a listview which I load upon the first page load. Since the contents of this list may change server side, I want to update this list every two seconds. I tried doing this by adding a setInterval call:
var OpenTicketListView = Backbone.View.extend({
el: '#the-id-in-the-template',
render: function() {
var that = this;
var tickets = new TicketColection();
tickets.fetch({
success: function(openTickets){
console.log('WE GOT A RESPONSE!');
var template = _.template($('#my-template').html(), {tickets: tickets.models});
that.$el.html(template);
}
});
}
});
var openTicketListView = new OpenTicketListView();
router.on("route:home", function() {
openTicketListView.render();
assignedTicketListView.render();
});
setInterval(openTicketListView.render, 2000);
This setup seems to ALMOST work. The listview renders perfectly fine the first time. The setInterval also seems to work, since it gets updated lists from the server, which look good (with updated content) in the console. I also see the "WE GOT A RESPONSE!" in the console. BUT, the only thing that it refuses to do, is update the view visually.
Does anybody know what I might be doing wrong here? What is the stupidity that I'm on here? How can I possibly debug this?
All tips are welcome!
The problem is, the value of this inside the render function is being lost when you call the function from setInterval. One way of solving that would be to bind the context to the function:
setInterval(openTicketListView.render.bind(openTicketListView), 2000);
Its worth noting, there is no need to call the render function directly. Instead, you can call fetch from the polling method, and have the view bind to the collection's sync event, and re-render itself. Something like this:
var OpenTicketListView = Backbone.View.extend({
el: '#the-id-in-the-template',
initialize: function() {
this.listenTo(this.collection, 'sync', this.render);
this.collection.fetch();
},
render: function() {
console.log('WE GOT A RESPONSE!');
var template = _.template($('#my-template').html(), {tickets: this.collection.models});
that.$el.html(template);
}
});
var tickets = new TicketColection();
var openTicketListView = new OpenTicketListView({ collection: tickets });
setInterval(tickets.fetch.bind(tickets), 2000);
Related
I have the following challenge:
I want to implement a loading flower every time I fetch data.
My bootstrapping code looks like this:
var myCollection = new MyCollection({});
myCollection.fetch({
success: function () {
var listView = new ListView({model: myCollection});
}
});
My view looks like this:
var ListView = Backbone.View.extend({
...
initialize: function () {
this.model.bind('request', this.ajaxStart, this);
this.model.bind('sync', this.ajaxComplete, this);
self.render();
},
});
My problem: The request event is not fired because at the time, the request starts, the ListView is not set up yet.
I tried to change my bootstrapping code but nothing works so far. It's always a chicken or the egg problem. Either I have the view set up at the right place or I have the data.
I tried to pass the collection instance to the view but this seems to be not possible via a set() or another method.
You probably should reorganize your structure, setting collection as a view model is not very obvious. Maybe you should add ListModel, that will handle your collection instantiating and updating, and then set your view to listen to your ListModel custom events. As for the chicken / egg problem
var myCollection = new MyCollection();
var myView = new MyView({
collection: myCollection,
});
myCollection.fetch();
// inside your view
initialize: function() {
this.listenTo(this.collection, 'sync', this.render);
}
I am trying to implement endless scrolling with Backbonejs. My view initializes a collection and calls fetch fetch function.
My view
var app = app || {};
app.PostListView = Backbone.View.extend({
el: '#posts',
initialize: function( ) {
this.collection = new app.PostList();
this.collection.on("sync", this.render, this);
this.collection.fetch();
this.render();
},
render: function() {
/*render posts*/
}
});
In my page I added the following code. It checks if the the user at the bottom of the page. If yes then it checks if the view is initialized. If yes then call that view fetch function of the view's collection object.
var app = app || {};
$(function() {
var post_view;
$(window).scroll(function() {
if(($(window).scrollTop() + $(window).height() == getDocHeight()) && busy==0) {
if(!post_view){
post_view = new app.PostListView();
} else {
post_view.collection.fetch();
}
}
});
});
So far this code is working. I am not sure if this is the right approach or not?
It's not a bad option; it works, and Backbone is making that collection available for you. But there's a couple of other options to consider:
Move that collection.fetch into a method getMoreItems() inside your PostListView, and call it within your else block. That way you're encapsulating your logic inside the view. Your app is more modular that way, and you can make your scrolling smarter without updating the rest of your app.
Move the scroll listener inside your PostListView. I'd probably put this within your PostListView's initialize function. Again, this reduces dependencies between the various parts of your app - you don't have to remember "Whenever I create a PostListView, I must remember to update it on scroll." By setting that event listener within the PostListView itself, all you have to do is create it. Backbone's general philosophy is to have small, independent components that manage their own state; moving the event listener inside would fit with that.
I had a view where I had a very basic list of registered users on the site, and some option like create, edit and delete all together on the same Backbone view and was working.
Since this is a learning exercise for me, now I wanted to decouple the main view form the details view or create view, so first thing I did was creating a new view for creating a user, which is a very basic form with 4 fields on it.
This is the code on the main view that calls the create user view
userCreate: function () {
event.preventDefault();
var createView = new viewAdminCV();
createView.render();
}
and this is the render function on the viewAdminCV() view
el: $('#frameDetails'),
render: function() {
this.$el.html(templateUserCreate);
}
There is no error if I execute this code, however nothing get rendered. If I replace the line
this.$el.html(templateUserCreate);
with
$('#frameDetails').html(templateUserCreate);
it works perfectly, but I want to understand why the $el is not working in this case, because I have it working in other views.
Appreciate you help.
In case is needed, the complete code is in this link
https://github.com/GabrielBarcia/UsersMod/blob/iss3/public/js/views/admin_CV.js
Basically, a you're finding the #frameDetails element "too early". Its easy to forget that when you use what they call a "jQuery Element" instance, you're really running a function. That means in your code, it's trying to find #frameDetails at the moment that you run "extend" on that Backbone.View. What you want is to find the element at new viewAdminCV() (I apologize if i sound confusing...).
BUT to fix this, there are three ways. One is traditionally, you just need to play in the literal string without the jQuery wrapper, then Backbone will find it:
el: '#frameDetails',
render: function() {
this.$el.html(templateUserCreate);
}
OR
you can wrap that with a handler
el: function(){
return $('#frameDetails')
},
render: function() {
this.$el.html(templateUserCreate);
}
OR an even slicker move, is you inject it from your "admin.js" file. Then you don't declare the "el" property in your viewAdminCV class:
userCreate: function () {
event.preventDefault();
$('#btnUserCreate').prop( 'disabled', true );
var createView = new viewAdminCV({ el: $('#frameDetails') });
createView.render();
}
You already have el as an object.
el: $('#frameDetails')
So you don't need to use $el while rendering.
Use:
el: $('#frameDetails'),
render: function() {
this.el.html(templateUserCreate);
}
From what I understand of the way Backbone.js is intended to be used, Views are supposed to be rendered in their own $el element, which is not necessarily attached to the page. If it is so, the higher level view they depend on usually takes care of inserting the $el in the page.
I am making this statement after having read the Todo sample application. In this case, the TodoView renders element in a default div element that is not attached to the page.
var TodoView = Backbone.View.extend({
// [...]
render: function() {
this.$el.html(this.template(this.model.toJSON()));
this.$el.toggleClass('done', this.model.get('done'));
this.input = this.$('.edit');
return this;
},
The AppView, upon a Todo creation, takes care of creating the TodoView and appending its $el to the page after rendering.
var AppView = Backbone.View.extend({
// [...]
addOne: function(todo) {
var view = new TodoView({model: todo});
this.$("#todo-list").append(view.render().$el);
},
My question is: If a view not attached to the page needs adjustments after being inserted (e.g. calculating its position in the viewport and performing DOM manipulation accordingly), where would you place the corresponding code?
Would you create a afterInsertion() method that the sublevel View should call after inserting, would you put the code at the same emplacement that where the insertion takes place (i.e. in the sublevel View) or would you change the way the view works to have it rendering directly in the page? I'm sure there are other solutions I can't think of right now. I would like to know what you consider being a best practice/optimized in the way Backbone should work, or if this question doesn't make sense to explain why.
I keep track of my sub-views. In a base view definition, create an add method:
var BaseView = Backbone.View.extend({
// It is really simplified... add the details you find necessary
add: function (name, viewDef, options) {
options = options || {};
if (!this.children)
this.children = {};
var view = new viewDef(options);
this.children[name] = view;
this.listenToOnce(view, 'ready', this.onSubViewReady);
view.render(options);
view.$el.appendTo(options.appendTo || this.$el);
}
});
With this, you can keep track of your subviews and make anything you want with them later.
If you feel like making things "automatic", you can trigger a ready event after your render method doing this:
var extend = BaseView.extend;
BaseView.extend = function (protoProps, staticProps) {
var child = extend.apply(this, arguments);
child.prototype.__render__ = protoProps['render'] || this.prototype.__render__ || function() {};
child.prototype.render = function () {
this.__render__.apply(this, arguments);
this.trigger('ready', this);
}
};
With this you can do a lot already.
Just remember that the DOM won't be drawn by the time that ready is triggered. So, if you are willling to do any calculations with the subview height or anything that needs the DOM to be drawn, use setTimeout(function () { ... }, 0) to put your code to the end of queue.
Hope I've helped.
I'm working on a Backbone app that renders several instances of the same view. Each of the views has a fairly large node tree and I can see that the user agent can't render the views instantaneously.
The issue I'm running into is that when my render callbacks fire, the height is coming through as 0 because the user agent's rendering engine hasn't actually finished rendering the entire view. If I set a timeout, the correct final height comes through:
var ChildView = window.Backbone.View.extend({
render: function() {
var template = require('templates/ChildTemplate');
this.$el.html(template());
this.afterRender();
},
afterRender: function() {
console.log(this.$el.outerHeight()); // 0
var _this = this;
setTimeout(function(){
console.log(_this.$el.outerHeight()); // 51 (or some non-zero integer)
}, 100);
setTimeout(function(){
console.log(_this.$el.outerHeight()); // 240, full correct height
}, 300);
}
});
How can I account for this rendering engine delay?
Architecture:
jquery 1.7.2
backbone 0.9.9
If the template contains images then i suggest you use the standard jquery load function
_this.$el.children('img').load(function(){
//the element should exist now
});
Otherwise im not sure what the problem is. Im fairly certain that Underscore's template function is synchronous so i wouldnt have suspected any problems there. One thing that might work, and probably makes more sense from a design perspective, would be to make use of initialize
var ChildView = window.Backbone.View.extend({
initialize: function(){
var template = require('templates/ChildTemplate');
this.renderedTemplate = template();
},
render: function() {
this.$el.html(this.renderedTemplate);
this.afterRender();
},
});
If you dont want to use the name renderedTemplate then thats fine, just make sure not to name it template as that is already in use