I am working on a relatively big application which is like some sort of app collection.
All of my apps got a bootstrapping view which loads the base layout and the nested views.
I now started to implement a singleton pattern to the views:
var SomeView = Backbone.View.extend({});
if (SomeView._instance) return SomeView._instance;
SomeView._instance = new SomeView();
return SomeView._instance;
Now I mentioned that when I switch between different apps (views) the event system is broken. This is actually quite logic finally we remove the view out of the document. However I have some sort of resistance against always building up the views new. This is quite ineffective: Everything has to get reloaded (data), and rebuilt.
So is there a way to rebind events to a cached views or is this whole idea bad and I should accept that views have to get rebuilt?
Update:
define(['jquery', 'underscore', 'backbone', 'views/settings/profile'], function($, _, Backbone, ProfileSettingsView) {
var ContentView = Backbone.View.extend({
tagName: "div",
className: "content well",
initialize: function() {
this.on('change:section', this.change, this);
this.views = {};
this.views.profile = new ProfileSettingsView();
},
render: function() {
this.$el.empty();
return this;
},
events: {
"click": "click"
},
// the router triggers this one here
change: function(query) {
console.log(query);
// if I uncomment this then nothing is rendered at all
//this.$el.detach();
var el;
if (query === 'profile') {
el = this.views.profile.render().el;
} else {
this.$el.empty();
}
if (el) {
this.$el.empty().append(el);
}
},
click: function(e) {
console.log('clicked, content should disapear');
}
});
if (ContentView._instance) return ContentView._instance;
ContentView._instance = new ContentView();
return ContentView._instance;
});
I am a bit confused about how I can use jQuery's detach().
I looked at the demo in the official docs and found out that it is not enough to call .detach() on a jQuery object. .detach returns a new object which looks like a jQuery one and contains the events bound. The hard thing about this is that I have to save this returnment of detach() somewhere and I have to now from who it's coming. And now I don't have any look through. I will now search for some Backbone.View example using detach() but I think it is to specific....
Update2:
Yes! I found a workaround: Instead of saving the events and then reinserting it in to the DOM. We can just call this.delegateEvents() to rebind all events. This truly is just a workaround and I would be happy if somebody could provide me an example :)
Personally, I prefer to rebuild my views.
However, I know a lot of people that prefer to re-use them. In that case, follow the instructions in this blog post from Tim Branyen: http://tbranyen.com/post/missing-jquery-events-while-rendering
Related
I've been trying to debug my Backbone multi-page app for most of the day now to get rid of 'zombies', but unfortunately to no avail. Before today, I didn't even realize I have a zombie problem. What am I doing wrong?
This is my RegionManager:
var regionManager = (function() {
var currView = null;
var rm = {};
var closeView = function(view) {
if (view && view.close) {
view.close();
}
};
var openView = function(view) {
view.render();
if (view.onShow) {
view.onShow();
}
};
rm.show = function(view) {
closeView(currView);
currView = view;
openView(currView);
};
return rm;
})();
This is my View cleaning up function:
Backbone.View.prototype.close = function() {
if (this.onClose) {
this.onClose();
}
if (this.views) {
_.invoke(this.views, 'close');
}
// Unbind any view's events.
this.off();
// Unbind any model and collection events that the view is bound to.
if (this.model) {
this.model.off(null, null, this);
}
if (this.collection) {
this.collection.off(null, null, this);
}
// Clean up the HTML.
this.$el.empty();
};
I tried appending the View els directly to the body and using this.remove(); in the View clean-up function (instead of using a common el: $('#content') to which I am appending elements, then cleaning up by this.$el.empty()), but that didn't work either.
It might have something to do with my "global Events":
Backbone.Events.on('letterMouseDown', this.letterMouseDown, this);
But I take care of them with the onClose function:
onClose: function() {
Backbone.Events.off('letterMouseDown');
}
One problem I see is that your close function never removes the event delegator from the view's el. A view's events are handled by using the delegator form of jQuery's on to attach a single event handler to the view's el. Your close does:
this.$el.empty();
but that only removes the content and any event handlers attached to that content, it does nothing at all to the handlers attached directly to this.el. Consider this minimal example:
var V = Backbone.View.extend({
events: {
'click': 'clicked'
},
clicked: function() {
console.log('still here');
}
});
var v = new V({ el: '#el' });
v.close();
After that, clicking on #el will throw a 'still here' in the console even though you think that the view has been fully cleaned up. Demo: http://jsfiddle.net/ambiguous/aqdq7pwm/
Adding an undelegateEvents call to your close should take care of this problem.
General advice:
Don't use the old-school on and off functions for events, use listenTo and stopListening instead. listenTo keeps track of the events on the listener so it is easier to remove them all later.
Simplify your close to just this:
Backbone.View.prototype.close = function() {
if(this.onClose)
this.onClose();
if(this.views)
_.invoke(this.views, 'close');
this.remove();
};
Don't bind views to existing els. Let the view create (and own) its own el and let the caller place that el into a container with the usual:
var v = new View();
container.append(v.render().el);
pattern. If you must attach to an existing el then the view should override remove with a slightly modified version of the standard implementation:
remove: function() {
this.$el.empty(); // Instead of removing the element.
this.undelegateEvents(); // Manually detach the event delegator.
this.stopListening();
return this;
}
I'm pretty sure I found the root for my problem.
mu is too short was right, with the close() method I wasn't removing the events bound directly to my el (which I tried to do by this.off() - this.$el.off()/this.undelegateEvents() is the correct way). But for me, it only fixed the problem that events got called multiple times unnecessarily.
The reason I was plagued by 'zombie views' or unintended behavior was that I wasn't freeing up the memory in the View..
this.remove() only gets rid of the el and it's elements/events, but not the View's internal variables. To elaborate - in my View I have an array declared like so this.array: [] and I didn't have it freed in the onClose function.
All I had to do was empty it in the onClose function or initially declare the array as this.array: null so on recurrent View renderings it would at least free the previous array (it still should be freed on the onClose method though, because the array/object is still going to sit in the memory until browsing away from the page).
It was excruciating to debug, because it's a crossword game (at least my code is hard to read there) and sometimes the words didn't match up, but I didn't know where the problem was coming from.
Lessons learned.
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);
I am still kind of new to backbone and I have a question. I am currently working on a previously created backbone view. This view is responsible for all the saving within the application. The class is becoming exceedingly large and I wanted to split out some of the events/methods/function to their own "sub class", if that is possible. For example, I have a group of events and functions that are responsible for a specific task. I would like to move those event and functions to their own js page, in a sub folder. The problem is that the events are being called. Here is a general overview of how the class is setup:
var myClass= myClassBaseClass.extend({
events: {
.... all my events here,
'click .eventOneButton': 'eventOne',
},
initialize: function (options) {
//initialize stuff here
},
postRender: function (renderOptions) {
//post render here
},
preRender: function (renderOptions, html) {
return html;
}, template: function (renderOptions) { //template stuff },
//...etc.
//my events
//eventOne was here, now its moved to its own file
});
My new file is in a subfolder, and looks like this:
var myClassSubClass= myClass.extend({
eventOne: function(e){
//event stuff here
}
});
So, is what am I doing wrong? Why wont the event get caught in the second file?
thanks
jason
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);
}
I'm looking for a simple event aggregator that works with require.js. I have two modules, one containing a view model and another with a "listener" of some sort:
// view model
define(['lib/knockout-2.2.1', 'events/aggregator'], function(ko, events){
var squareViewModel = function(contents) {
this.click = function(){
events.publish('squareClicked', this);
};
};
return squareViewModel;
});
// some listener of some kind
define(['events/aggregator'], function(events){
events.subscribe('squareClicked', function(e){
alert("hurrah for events");
});
});
Is there anything out there that does this? Is this kind of architecture even a good idea? This is my first foray into client-side architecture.
This is similar to what you posted, but I've had good luck with extending Backbone events (you don't actually have to use anything else about Backbone for this to work), something like this:
define(['underscore', 'backbone'], function( _, Backbone ) {
var _events = _.extend({}, Backbone.Events);
return _events;
});
And then all your viewmodels (or any code really) can use it:
define(['knockout', 'myeventbus'], function(ko, eventBus){
return function viewModel() {
eventBus.on('someeventname', function(newValue) {
// do something
});
this.someKOevent = function() {
eventBus.trigger('someotherevent', 'some data');
};
};
});
The original idea came from this article by Derick Bailey. One of my other favorites on client-side events is this one by Jim Cowart
Again, it amounts to nearly the same thing as what you posted. However, the one thing I don't like about the approach of tying it to jQuery document DOM node is that you might have other types of events flying through there as well, bubbled up from child nodes, etc. Whereas by extended backbone events you can have your own dedicated event bus (or even more than one if you e.g. wanted to have separate data events vs. UI events).
Note regarding RequireJS in this example: Backbone and Underscore are not AMD-compatible, so you'll need to load them using a shim config.
I ended up using jQuery to make my own:
define([], function(){
return {
publish: function (type, params){
$(document.body).trigger(type, params);
},
subscribe: function(type, data, callback){
$(document.body).bind(type, data, callback);
},
};
});
It works for what I want it for, but it has not been extensively tested.
As explunit points out in his answer, this will capture any events on document.body. I also discovered a scoping issue when accessing this inside of a passed callback function.