I've extended Backbone's View prototype to include a "close" function in order to "kill the zombies", a technique I learned from Derrick Bailey's blog
The code looks like this:
Backbone.View.prototype.close = function () {
this.remove();
this.unbind();
if (this.onClose) {
this.onClose();
}
};
Then I have a Router that looks (mostly) like this:
AppRouter = Backbone.Router.extend({
initialize: function() {
this.routesHit = 0;
//keep count of number of routes handled by your application
Backbone.history.on('route', function () { this.routesHit++; }, this);
},
back: function () {
if(this.routesHit > 1) {
//more than one route hit -> user did not land to current page directly
logDebug("going window.back");
window.history.back();
} else {
//otherwise go to the home page. Use replaceState if available so
//the navigation doesn't create an extra history entry
this.navigate('/', {trigger:true, replace:true});
}
},
routes: {
"": "showLoginView",
"login": "showLoginView",
"signUp": "showSignUpView"
},
showLoginView: function () {
view = new LoginView();
this.render(view);
},
showSignUpView: function () {
view = new SignUpView();
this.render(view);
},
render: function (view) {
if (this.currentView) {
this.currentView.close();
}
view.render();
this.currentView = view;
return this;
}
});
The render function of my LoginView looks like this:
render: function () {
$("#content").html(this.$el.html(_.template($("#login-template").html())));
this.delegateEvents();
return this;
}
The first time the LoginView is rendered, it works great. But if I render a different view (thereby calling "close" on my LoginView) and then try to go back to my LoginView, I get a blank screen. I know for a fact that the render on my LoginView fires the second time, but it seems that my "close" method is causing a problem. Any ideas?
EDIT After some feedback from Rayweb_on, it appears I should add more detail and clarify.
My HTML looks like this:
<div id="header">this is my header</div>
<div id="content">I want my view to render in here</div>
<div id="footer">this is my footer</div>
Then I have a login-template that looks like this (sort of):
<script type="text/template" id="login-template">
<div id="login-view">
<form>
...
</form>
</div>
</script>
I'm trying to get it so that the view always renders inside of that "content" div, but it appears that the call to "close" effectively removes the "content" div from the DOM. Hence the "blank" page. Any ideas?
EDIT 2 Here's what my LoginView looks like, after some noodling:
LoginView = Backbone.View.extend({
events: {
"vclick #login-button": "logIn"
},
el: "#content",
initialize: function () {
_.bindAll(this, "logIn");
},
logIn: function (e) {
...
},
render: function () {
this.$el.html(_.template($("#login-template").html()));
this.delegateEvents();
return this;
}
});
I set the el to "#content" in the hopes that it would get recreated. But still no luck. In fact, now when I go to the next page it's not there because #content is being removed right away.
I also tried:
LoginView = Backbone.View.extend({
events: {
"vclick #login-button": "logIn"
},
el: "#login-template",
initialize: function () {
_.bindAll(this, "logIn");
},
logIn: function (e) {
...
},
render: function () {
this.$el.html(_.template($("#login-template").html()));
this.delegateEvents();
return this;
}
});
But that doesn't work at all. Any ideas?
When you remove your view the first time you are removing its el, so this line
$("#content").html(this.$el.html(_.template($("#login-template").html())));
on your render function wont work. as this.$el. its undefined.
It would appear that any elements included in the view get destroyed (or at least removed from the DOM) by my "close" function (thanks Rayweb_on!). So, if I reference the #content div in my view, I lose it after the view is closed. Therefore, I no longer reference #content anywhere in my view. Instead, I add my view's default "el" to my #content element externally of my view.
In my case, I chose to do it in my router. The result looks like this:
ROUTER
AppRouter = Backbone.Router.extend({
initialize: function() {
...
},
routes: {
"" : "showLoginView",
"login": "showLoginView",
"signUp": "showSignUpView",
},
showLoginView: function () {
view = new LoginView();
this.render(view);
},
showSignUpView: function () {
view = new SignUpView();
this.render(view);
},
render: function (view)
{
if (currentView)
{
currentView.close();
}
$("#content").html(view.render().el);
currentView = view;
return this;
}
});
LOGIN VIEW
LoginView = Backbone.View.extend({
events: {
"vclick #login-button": "logIn"
},
initialize: function () {
_.bindAll(this, "logIn");
},
logIn: function (e) {
...
},
render: function () {
this.$el.html(_.template($("#login-template").html()));
this.delegateEvents();
return this;
}
});
This all works swimingly, but I am not certain it's the best solution. I'm not even certain it's a good solution, but it'll get me moving for now. I'm certainly open to other options.
====
EDIT: Found a slightly cleaner solution.
I wrote a function called injectView that looks like this:
function injectView(view, targetSelector) {
if (_.isNull(targetSelector)) {
targetSelector = "#content";
}
$(targetSelector).html(view.el);
}
and I call it from each View's render function, like this:
render: function () {
this.$el.html(_.template($("#login-template").html()));
this.delegateEvents();
injectView(this,"#content")
return this;
}
It works. I don't know how good it is. But it works.
Related
Is it possible to make a set of default events which exist in every view? For example if every view in my application includes a settings button
events: {
"click #settings" : "goSettings"
},
...
goSettings: function() {
// settings show function
});
How can I can package this event to be included in every view in my application?
The problem is that View#extend simply overwrites existing properties so you can't put your 'click #settings' in a base class and subclass that. However, you can easily replace extend with something of your own that merges events. Something like this:
var B = Backbone.View.extend({
events: {
'click #settings': 'goSettings'
}
}, {
extend: function(properties, classProperties) {
properties.events = _({}).extend(
properties.events || { },
this.prototype.events
);
return Backbone.View.extend.call(this, properties, classProperties);
}
});
And then extend B instead of Backbone.View for your views.
Demo: http://jsfiddle.net/ambiguous/Kgh3V/
You can create a base view with the event(s) and functions, then make your other views inherit from it. I like the pattern described here, because it's simple to set up and easy to override as needed: http://www.scottlogic.com/blog/2012/12/14/view-inheritance-in-backbone.html
A base view looks like this:
var BaseSearchView = function(options) {
this.inheritedEvents = [];
Backbone.View.call(this, options);
}
_.extend(BaseView.prototype, Backbone.View.prototype, {
baseEvents: {},
initialize: function(options) {
// generic initialization here
this.addEvents({
"click #settings" : "goSettings"
});
this.initializeInternal(options);
},
render: function() {
// generic render here
this.renderInternal();
return this;
},
events: function() {
var e = _.extend({}, this.baseEvents);
_.each(this.inheritedEvents, function(events) {
e = _.extend(e, events);
});
return e;
},
addEvents: function(eventObj) {
this.inheritedEvents.push(eventObj);
},
goSettings: function() {
// settings show function
}
});
BaseView.extend = Backbone.View.extend;
And your child classes like this:
var MyView = BaseView.extend({
initializeInternal: function(options) {
// do something
// add event just for this child
this.addEvents({
"click #differentSettings" : "goSettings"
});
},
renderInternal: function() {
// do something
}
});
So I have this current situation:
app.Ui.ModalView = Backbone.View.extend({
events: {
},
initialize: function() {
},
render: function() {
var that = this;
var model = this.model.toJSON();
that.$el.html(that.template(_.extend(this.params || {}, {
model: model,
})));
return this;
}
});
and then the inherited view:
app.Views.childView = kf.Ui.ModalView.extend({
template: JST["templates/app/blah/blah-edit.html"],
events: {
},
initialize: function() {
var that = this;
this.events = _.extend({}, app.Ui.ModalView.prototype.events, this.events);
app.Ui.ModalView.prototype.initialize.apply(this, arguments);
},
render: function(){
// add extra logic in this render function, to run as well as the inherited render function?
}
});
So, I don't want to override the parent's render(), but to add extra functionality to it, how would I go about doing that?
Two ways to achieve this: Either you can add explicit support for overriding the behaviour by creating a "render hook" in the base class, or you'll have to call the overridden base method from the superclass method:
Render hook in base class:
app.Ui.ModalView = Backbone.View.extend({
render: function() {
//if this instance (superclass) defines an `onRender` method, call it
if(this.onRender) this.onRender();
//...other view code
}
}
app.Views.childView = kf.Ui.ModalView.extend({
onRender: function() {
//your custom code here
}
});
Call base class method from super class:
app.Views.childView = kf.Ui.ModalView.extend({
render: function() {
//your custom code here
//call the base class `render` method
kf.Ui.ModalView.prototype.render.apply(this, arguments);
}
});
I have a page which include two backbone views (views related to two template). I am changing content of one views based on clicking event on different items on another view. For this, Every time I click on any items in one view I just create a instance of another view which include some socket.io events. At the first time It's work well but everytime I click on item on first view it just create the instance of 2nd one so that all the socket.io events is binding. Except first click every time I click on items on first view and call an socket.io events, it fired more than one time based on how many click I have done to different items.
I know that every time I click an items it create an instance of a view with socket.io event bind. But I can not get the way to unbind the previous socket.io events.
I have tried to use this reference:
Backbone.js View removing and unbinding
But it is not working in my case. May be I did not use it in proper way.
Can anyone please give me a solution or way to unbind all the socket.io events binded before?
Here is my Clicking event from where I am creating a new instance of another view where all the socket.io events binds.
LoadQueueDetails: function (e) {
e.preventDefault();
var queues = new Queues();
queues.fetch({
data: $.param({ Code: this.model.get("QueueCode") }),
success: function () {
$("#grid21").html(new SearchResultListView({ collection: queues }).el);
},
error: function (queues) {
alert('error found in fetch queue details');
}
});
}
And here is my actual view where I bind all the socket.io events.
window.SearchResultListView = Backbone.View.extend({
initialize: function () {
this.collection.on('change', this.render, this);
this.render();
},
render: function () {
var Queues = this.collection;
var len = Queues.length;
$(this.el).html(this.template());
for (var i = 0; i < len; i++) {
$('.QueueListItem', this.el).append(new SearchResultListItemView({ model: Queues.models[i]}).render().el);
}
return this;
}
});
window.SearchResultListItemView = MainView.extend({
tagName: "tr",
initialize: function () {
this.__initialize();
var user;
if ($.super_cookie().check("user_cookie")) {
this.user = $.super_cookie().read_JSON("user_cookie");
}
this.model.bind("change", this.render, this);
this.model.on("destroy", this.close, this);
socket.emit('adduser', this.user.UserName, this.model.get("Code"));
},
events: {
"click a": "JoinQueue"
},
onClose: function(){
this.model.unbind("change", this.render);
},
close: function () {
this.remove();
this.unbind();
this.model.unbind("change", this.render);
},
socket_events: {
"updatechat": "updatechat",
"changeroom": "changedroom"
},
changedroom: function (username, data) {
alert(data);
socket.emit('switchRoom', data);
},
updatechat: function (username, data) {
alert(username);
alert(data);
},
JoinQueue: function (e) {
e.preventDefault();
if ($.super_cookie().check("user_cookie")) {
user = $.super_cookie().read_JSON("user_cookie");
}
socket.emit('sendchat', "new user");
},
render: function () {
var data = this.model.toJSON();
_.extend(data, this.attributes);
$(this.el).html(this.template(data));
return this;
}
});
window.Queue = Backbone.Model.extend({
urlRoot: "/queue",
initialize: function () {
},
defaults: {
_id:null,
Code: null,
ServiceEntityId: null,
ServiceEntityName:null,
Name: null,
NoOfWaiting: null,
ExpectedTimeOfService: null,
Status: null,
SmsCode: null
}
});
window.Queues = Backbone.Collection.extend({
model: Queue,
url: "/queue",
initialize: function () {
}
});
Backbone.View.prototype.close = function () {
this.remove();
this.unbind();
if (this.onClose) {
this.onClose();
}
}
And this is my main view to bind socket.io event in searchResultItemview.
var MainView = Backbone.View.extend({
initialize: function () {
this.__initialize();
},
__initialize: function () {
if (this.socket_events && _.size(this.socket_events) > 0) {
this.delegateSocketEvents(this.socket_events);
}
},
delegateSocketEvents: function (events) {
for (var key in events) {
var method = events[key];
if (!_.isFunction(method)) {
method = this[events[key]];
}
if (!method) {
throw new Error('Method "' + events[key] + '" does not exist');
}
method = _.bind(method, this);
socket.on(key, method);
};
}
});
For extra information:
1. I am opening socket connection globally. Like this :
var socket = io.connect('http://localhost:3000');
I am waiting for any kind of advice or solution to get out of this problem. Please feel free to ask any kind of inquiries.
Basically you have to do socket.removeListener for every socket.on when you close your View.
You can update your MainView and add a close method.
This is how it looks in my code (CoffeeScript)
close: ->
self = #
_.each #socket_events, (method, key) ->
method = self[self.socket_events[key]]
socket.removeListener key, method
I'm having a problem with rendering a backbone.js view successfully from a route handler (browser application).
My javascript module is currently setup like this:
$(function () { // DOM ready
myModule.Init();
});
var myModule = (function () {
// Models
var DonorCorpModel = Backbone.Model.extend({ });
// Collections
var DonorCorpsCollection = Backbone.Collection.extend({ model : DonorCorpModel });
// Views
var DonorCorpsListView = Backbone.View.extend({
initialize : function () {
_.bindAll(this, 'render');
this.template = Handlebars.compile($('#pre-sort-actions-template').html())
this.collection.bind('reset', this.render);
},
render : function () {
$(this.el).html(this.template({}));
this.collection.each(function (donorCorp) {
var donorCorpBinView = new DonorCorpBinView({
model : donorCorp,
list : this.collection
});
this.$('.donor-corp-bins').append(donorCorpBinView.render().el);
});
return this;
}
});
var DonorCorpBinView = Backbone.View.extend({
tagName : 'li',
className : 'donor-corp-bin',
initialize : function () {
_.bindAll(this, 'render');
this.model.bind('change', this.render);
this.template = Handlebars.compile($('#pre-sort-actions-donor-corp-bin-view-template').html());
},
render : function () {
var content = this.template(this.model.toJSON());
$(this.el).html(content);
return this;
}
})
// Routing
var App = Backbone.Router.extend({
routes : {
'' : 'home',
'pre-sort' : 'preSort'
},
initialize : function () {
// ...
},
home : function () {
// ...
},
preSort : function () {
if (donorCorps.length < 1) {
donorCorps.url = 'http://my/api/donor-corps';
donorCorps.fetch();
}
var donorCorpsList = new DonorCorpsListView({ collection : donorCorps }).render().el;
$('#document-action-panel').empty().append(donorCorpsList);
// ...
}
});
// Private members
var app;
var donorCorps = new DonorCorpsCollection();
// Public operations
return {
Init: function () { return init(); }
};
// Private operations
function init () {
app = new App();
Backbone.history.start({ root: '/myApp/', pushState: true });
docr.navigate('/', { trigger: true, replace: true});
}
}(myModule || {}));
Everything runs just fine when I run the app...it navigates to the home view as expected. I have links setup with handlers to navigate to the different routes appropriately, and when I run app.navigate('pre-sort', { trigger: true, replace: true}) it runs just fine, and renders the expected items in the list as expected.
The problem comes when I try to go back to the home view, and then back to the pre-sort view again...the items in the list don't get displayed on the screen as I am expecting them to. I'm just getting the empty pre-sort-actions-template rendered with no child views appended to it. I've stepped through the code, and can see that the collection is populated and the items are there...but for some reason, my code isn't rendering them to the view properly, and I can't seem to figure out why or what I'm doing wrong. Any ideas?? I'm pretty new to backbone, so I'm sure this code isn't written totally right...constructive feedback is welcome. Thanks.
how is the code rendering it to the view after going back home, then to pre-sort again? could you provide some details on that? duplicate items? empty view?
Also, I like to keep an index of my sub-views when I render them so the parent view can always access them regardless of scope. I found a really nice technique for this here: Backbone.js Tips : Lessons from the trenches
A quick overview of the pattern I'm referring to is as follows:
var ParentView = Backbone.View.extend({
initialize: function () {
this._viewPointers = {};
},
render: function (item) {
this._viewPointers[item.cid] = new SubView ({
model: item
});
var template = $(this._viewPointers[item.cid].render().el);
this.$el.append(template);
}
});
var SubView = Backbone.View.extend({
initialize: function () {
this.model.on("change", this.render, this);
},
render: function () {
this.$el.html( _.template($('#templateId').html(), this.model.attributes) );
return this;
}
});
I realize this answer is rather "broad," but it will be easier to answer with more specifics if I can understand the exact issue with the rendering. Hope its of some help regardless :)
I'm trying to use Backbone.js to in a JQuery Dialog. I've managed to get the dialog to render and open, but it doesn't seem to be firing my events. I've added a test event to check this, and clicking it doesn't have the expected result.
I've tried following the instructions on this blogpost, regarding delegateEvents, but nothing it made no difference. No errors are thrown, the events just don't fire. Why is this?
Slx.Dialogs.NewBroadcastDialog.View = Backbone.View.extend({
events: {
"click .dialog-content": "clickTest"
},
clickTest : function () {
alert("click");
},
render: function () {
var compiledTemplate = Handlebars.compile(this.template);
var renderedContent = compiledTemplate();
var options = {
title: Slx.User.Language.dialog_title_new_message,
width: 500
};
$(renderedContent).dialog(options);
this.el = $("#newBroadCastContainer");
this.delegateEvents(this.events);
return this;
},
initialize: function () {
_.bindAll(this, 'render');
this.template = $("#newBroadcastDialogTemplate").html();
this.render();
}
});
You might want to try this. I had to refactor your code a bit hope you will get the idea
Slx.Dialogs.NewBroadcastDialog.View = Backbone.View.extend({
el:"#newBroadCastContainer",
template:$("#newBroadcastDialogTemplate").html(),
events: {
"click .dialog-content": "clickTest"
},
clickTest : function () {
alert("click");
},
render: function () {
var compiledTemplate = Handlebars.compile(this.template);
var renderedContent = compiledTemplate();
$(this.el).html(renderedContent).hide().dialog(this.options.dialogConfig);
return this;
},
initialize: function () {
}
});
Instantiate and render outside the View definition
var myDialog = new Slx.Dialogs.NewBroadcastDialog.View({dialogConfig:{title: Slx.User.Language.dialog_title_new_message,width: 500}});
myDialog.render();
The problem turned out to be due to me assigning this.el when I should have been assigning this.$el
This worked perfectly:
Slx.Dialogs.NewBroadcastDialog.View = Backbone.View.extend({
el: "#newBroadcastContainer",
events: {
"click .clicktest": "clickTest"
},
clickTest : function () {
console.log("click");
},
render: function () {
var compiledTemplate = Handlebars.compile(this.template);
var renderedContent = compiledTemplate();
var options = {
title: Slx.User.Language.dialog_title_new_message,
width: 500
};
this.$el = $(renderedContent).dialog(options);
return this;
},
initialize: function () {
_.bindAll(this, 'render');
this.template = $("#newBroadcastDialogTemplate").html();
this.render();
}
});
I had two codebases on one of the code base I was able to bind events by assigning the dialog to this.$el however in the other codebase this somehow did not work. I add the following line this.el = this.$el;
to the code and it is working now. however I am still not able to figure out why it was working in one codebase and not the other and why assigning $el to el got it to work.