I've read Derick Bailey's article on Zombies but can't seem to figure it out. I have a backbone application that uses require.js and need to be able to close/destroy a view when I navigate away from it.
There's a lot of ways to initiate a backbone app, but what is the right way when using require to allow for clean up?
And how can I call a close() function on views just before navigating away?
Main.js
require([ "app", "backbone", "router", "facebook"], function(App, Backbone, Router, FB) {
//theres a lot of facebook integration
FB.init({
appId : '********',
version : 'v2.0'
});
//force the page to go to index when first arriving
window.location.hash = "";
new App;
Backbone.history.start();
});
App.js
define([ "backbone", "router" ], function(Backbone, Router){
var App = function () {
Router;
}
return App;
})
Router.js
define([ "backbone", "models/user" ], function(Backbone, User){
var AppRouter = Backbone.Router.extend({
routes: {
//All my routes
},
index: function () {
require([ "views/index", "models/user" ], function (IndexView, UserModel) {
var indexView = new IndexView({ model: UserModel });
})
},
// Remaining route functions
return new AppRouter;
})
That post solves a problem that your code here doesn't have.
The problem (zombie views) only occurs when your views have attached event handlers to a model instance.
var View = Backbone.View.extend({
setup: {
// model instance will now be storing a callback which is bound
// to *this* instance of a view
this.model.on('change', this.render, this);
},
render: function () {
// whatever code that uses the context, `this`
this.$el.innerHTML(this.model.get('title'));
}
});
Then in your app lifetime, the above view got rendered and then the page changed, or whatever happened, and the view is not needed anymore. But there might anything else that is using the model that this view has used – and that model might keep changing and then it will fire a callback for change event, the render method which will point at seemingly non-existent view.
But since that view might not have its element in the DOM anymore, you'll get DOM errors (if for example your render method referenced this.$el.parent()) and the views will remain in memory without you knowing it, eventually causing your page to get slow or even unresponsive.
Since that post was written there's now a new way of attaching event handlers, called listenTo, which makes it easier to stopListening.
There's also now View.prototype.remove method which will remove the view's element from the DOM and also call stopListening which will help in case you used listenTo to attach event handlers for the models.
Related
I am trying to load one html file using backbone js and require js file .I am able to call initialise function of view but not able to load that html file here is my code
define([
'jquery',
'underscore',
'backbone',
'text!templates/stats.html'
], function ($, _, Backbone, statsTemplate) {
'use strict';
var AppView = Backbone.View.extend({
// Instead of generating a new element, bind to the existing skeleton of
// the App already present in the HTML.
el: '#todoapp',
// Compile our stats template
template: _.template(statsTemplate),
// Delegated events for creating new items, and clearing completed ones.
events: {
},
// At initialization we bind to the relevant events on the `Todos`
// collection, when items are added or changed. Kick things off by
// loading any preexisting todos that might be saved in *localStorage*.
initialize: function () {
alert('-in--')
},
// Re-rendering the App just means refreshing the statistics -- the rest
// of the app doesn't change.
render: function () {
}
// Add a single todo item to the list by creating a view for it, and
// appending its element to the `<ul>`.
});
return AppView;
})
I am getting the alert in initialize function not able to load the html file
here is my code
http://plnkr.co/edit/rhoE1H9A8nfu6II64aEo?p=preview
Thanks for taking the time to set up a working example.
Unfortunately Backbone doesn't give you much for free, so there are a number of manual steps to get this working:
Add a <div id="todoapp"></div> to index.html as you are targeting it with el: '#todoapp' but it doesn't exist.
Doing template: _.template(statsTemplate) will return a compiled version of the template (as a function). You then need to call it like a function, optionally passing in a context so that the template can render dynamic data. e.g. this.template().
The render method won't get called automatically, so when you are ready to render the view (usually instantly but it could be after an AJAX response) you need to call this.render(). In your case straight away in initialize.
Finally in render you can attach the rendered template to the view's element: this.$el.html(this.template());
Updated example: http://plnkr.co/edit/6HyOhuQ7LGL91rS8slJX?p=preview
I recommend you create a BaseView with this common render flow so you don't have to repeat it every time. Also it's a good idea in that BaseView to set up a concept of sub views which clean up properly when the parent's remove is called, and re-render when a parent's render is called.
Here is an example of using a BaseView: http://jsfiddle.net/ferahl/xqxcozff/1/
I've hit a head-scratcher with a Backbone.js. The example is on jsfiddle here. I believe the issue is here:
App.Layout = new Backbone.Layout({
// Attach the Layout to the main container.
collection: App.chapters,
el: "body",
initialize: function () {},
beforeRender: function () {
// Add a sub-view for each Chapter
this.collection.each(function (model) {
this.insertView(model.get('id'), new App.ChapterView({
"id": model.get('id')
}));
}, this);
},
views: {
// But if I set the sub-view specifically if works
// "one": new App.ChapterView({id: 'one' })
}
});
In summary, the router should simply activate or deactivate backbone.layoutmanager sub-views based on the path, e.g., /#chapter/one, /#chapter/two, etc.
If I explicitly set the sub-views in App.Layout (see line 49 in the fiddle), the routing works as expected.
However, if I try to add the views by iterating a collection of models in the beforeRender function (line 40; beforeRender is coming from backbone.layoutmanager), they don't appear to be available when the router tries to find the matching view by ID.
Once the page has render, however, the view can be activated with:
App.router.navigate('/chapter/two',{"trigger": true});
Which seems to indicate that the views are properly being added and should be findable by the router with:
App.Layout.getView(name);
No doubt I'm simply overlooking something, or am about to expose my ignorance of the Backbone library. :)
The issue is that you're navigating and rendering out-of-sync. I've updated your code here: http://jsfiddle.net/6h268r7j/55/
It works when you use the declarative approach because those are outside of the render flow, essentially statically added. As soon as you use beforeRender/render you are now in an asynchronous render flow and they won't be available in your router callbacks.
The fix was to simply render the application layout first and then trigger the routing:
App.Layout.render().then(function() {
Backbone.history.start();
});
tl;dr: When moving from page to page, change/destroy only these blocks that need it (not re-rendering whole page). And keep application router as simple as possible.
I'm new to backbone and all of the examples to get started with it that I've seen were about small apps with a single page that changes a little sometimes (adding/removing some elements, but never completely re-rendering). But when I started doing my app which is a little bit more complex, I faced the problem of subviews organization...
Problem: When every page of application consists of subviews (each subview can have another subviews, ie nested subviews), which are responsible for displaying their blocks in the page, a reasonable desire would be not to re-render blocks that not changing when you're navigating through pages. Sometimes you need to re-render whole page, when it's completely different from previous, sometimes just some blocks on the page. But in this case application router becomes monster-object that contains too much logic, so that it's hard to maintain.
What I want: I want my router to be like
define(['jquery', 'backbone'], function ($, Backbone) {
return Backbone.Router.extend({
routes: {
"": "home",
..........
},
home: function () {
require(['views/home'], function (View) {
var view = new View({ el: $("body") });
view.render();
});
},
..........
});
});
So, my goal is to move logic for rendering subviews to views. So that view will decide what to do: render itself including subviews or just ask subviews to decide same question for themselves.
Possible solution: Thinking about it I come with idea of global object that contains tree of views as application state (e.g. first_design(home(about, login)) ). And when we're moving to next page, that has the same main view (ie first_design) we don't render first_design view, but just it's subviews (in this case only home). And for every page in router we now need to manually define this tree of views. For example like this:
define(['jquery', 'backbone'], function ($, Backbone) {
return Backbone.Router.extend({
routes: {
"": "home",
"contacts": "contacts",
..........
},
home: function () {
require(['views/firstDesign', 'views/home', 'views/about', 'views/login'], function (FirstDesign, Home, About, Login) {
new FirstDesign({ el: $("body"), subviews: { new Home({ subviews: { new About(), new Login() } }) } });
});
},
contacts: function () {
require(['views/firstDesign', 'views/contacts'], function (FirstDesign, Contacts) {
new FirstDesign({ el: $("body"), subviews: { new Contacts() } });
});
},
..........
});
});
So, the question is...: I believe i'm reinventing the wheel right now and obviously my wheel is not good enough (possibly even not round :D). So are there any implementations of what I need? Or if not, then how I should do it properly? Thanks!
P.S: I'm using backbone.js with require.js. And sorry for my English, it's not my native language...
There is a Backbone plugin called LayoutManager. From their wiki page:
LayoutManager provides a logical foundation for assembling layouts and
views within Backbone.
I use it and it serves me well.
https://github.com/tbranyen/backbone.layoutmanager/wiki
I realized that my solution is not so flexible. And after all I decided to use Marionette. What really helped me to solve my problem is this question. p.s: Marionette has now a hasView method in Region class.
In Backbone, is there any way to trigger a route event handler, without changing the URL?
What I mean is that I want to trigger a route handler, but I don't want to change the URL.
Hence, I don't want to use
router.navigate(route, {trigger: true});
as this will cause the URL to change.
The router itself is connected to a function. The simple answer is to call the function straight away, simply bypassing the route handling.
Example
(function( $, Backbone ) {
var exports = window.app = window.app || {},
Router = Backbone.Router.extend({
// Here you declare what the routes are in your router
// and what functionality they should trigger.
routes: {
"help" : "help",
"search/:query" : "search",
"search/:query/p:page": "search"
},
// Declare route functions.
help : function() {},
search: function( query, page ) {}
});
// Export the router.
exports.router = new Router();
// Just a dummy object for calling the router.
var cookieMonster = {
init: function() {
// Do something on init.
// End with calling the route help function.
exports.router.help();
}
};
}(jQuery, Backbone));
cookieMonster.init() would in this case end with a call to the help function in the router.
A tip is to look at Backbone Marionette where you have a Controller which has the function logic seperated from the routes, one of many things that make Marionette awesome.
For what its worth, Marionette routing is explained extensively here: http://samples.leanpub.com/marionette-gentle-introduction-sample.pdf
The strategy that is discussed is separating URL management from application reactions (e.g. switching sub-applications). This means that you're then free to have your app trigger a handler (using a Marionette event) without modifying the URl fragment.
Have you tried Backbone.history.loadUrl(route);?
I am working on a single-page app using Backbone.js. An issue that has occurred to me is that since one is not reloading the page, that when one creates a instance of a View, then I assume, that the View object will remain in memory for the life of the app. This does not seem very efficient to me, since a particular view may no longer be needed if another route is called. However, a particular View may later need to be 'displayed' if one returns to that original route. So the question is, how to best manage views in Backbone with regards to routes?
In my app, many of the views are responsible for displaying a particular 'page' and as such share the same DOM element. When one of these 'page' views is called, it will replace the content in the DOM element previously put in place by the previous view. Thus the previous view is no longer needed.
Do I need to somehow manually destroy the previous View (or is this somehow handled by the Router object)? Or is it better to leave the views once they have been initialized?
Following sample code shows how views instances are being creating in the Router in the app.
/**
* View - List of contacts
*/
var ListContactsView = Backbone.View.extend({
el: '#content',
template: _.template($('#list-contacts-tpl').html()),
initialize: function() {
_.bindAll(this, 'render');
this.collection = new Contacts();
this.collection.bind('reset', this.render);
this.collection.fetch();
},
render: function() {
this.$el.hide();
this.$el.html(this.template({ contacts: this.collection }));
this.$el.fadeIn(500);
}
});
/**
* View - Display single contact
*/
var DisplayContactView = Backbone.View.extend({
el: '#content',
events: {
'click #delete-contact-button': 'deleteContact'
},
template: _.template($('#display-contact-tpl').html()),
initialize: function() {
_.bindAll(this, 'deleteContact', 'render');
// Create reference to event aggregator object.
if (typeof this.options.id === 'undefined') {
throw new Error('View DisplayContactView initialized without _id parameter.');
}
this.model = new Contact({ _id: this.options.id });
// Add parse method since parsing is not done by collection in this
// instance, as this model is not called in the scope of collection
// Contacts.
this.model.parse = function(response) {
return response.data;
};
this.model.bind('change', this.render);
this.model.fetch();
},
deleteContact: function(id) {
// Trigger deleteContact event.
this.eventAggregator.trigger('deleteContact', id);
},
render: function() {
this.$el.html(this.template({ contact: this.model.attributes }));
}
});
/**
* Page routes
*/
var $content = $('#content');
var ClientSideRouter = Backbone.Router.extend({
routes: {
'browse': 'browse',
'browse/view/:id': 'browseViewContact',
'orgs': 'orgs',
'orgs/:orgName': 'orgs',
'orgs/:orgName/:id': 'orgs',
'contact/add': 'addContact',
'contact/view/:id': 'viewContact',
'contact/delete/:id': 'confirmDelete',
'*path': 'defaultPage'
},
addContact: function() {
// Display contact edit form.
var editContactFormView = new EditContactFormView();
// Display email field in edit form.
},
browse: function() {
var listContactsView = new ListContactsView();
},
browseViewContact: function(id) {
var displayContactView = new DisplayContactView({ id: id });
},
defaultPage: function(path) {
$content.html('Default');
},
home: function() {
$content.html('Home');
},
viewContact: function(id) {
$.ajax({
url: '/contact/view/' + id,
dataType: 'html',
success: function(data) {
$content.html(data);
}
});
}
});
var clientSideRouter = new ClientSideRouter();
Backbone.history.start();
Routes do not destroy views
Routes provide you convenient manner to interact with url changes. By convenience i mean url semantics and context of current page. For example url #/!/create/ will invoke a method that should display a form to create a model. Context here is the view to create model.
Views should be managed by the developer
there still does not exists a well known manner to manage views in Backbone.js, but i prefer the way of global variables. This would ensure your view instances are available throughout application and all the modules have access to them. For example doing this
window.App.Contacts.ContactView = new App.Contacts.View.ContactView({model:BenContact}); will make view used to display Ben's contact information available to application modules through window object. All you need to do for any views that use same el is to destroy the ContactView and render the new view.
You have methods on view to remove them
Undelegate Events and Remove methods help you remove them. Inside the callback method that handles routes hash change events. For example in the callback method that handles #/!/view/all ( url to view all the contacts list) you might come across situation where both the views now use the same el so you should destroy the ContactView and render ListView so in the callback do this
App.Contacts.ContactView.undelegateEvents();
App.Contacts.ContactView.remove();
Since Backbone.js has no built in support for view compositions, there are several patterns that you could follow when it comes to keeping track of child views.
Derick Bailey illustrates extending Backbone.View to allow views to
clean up after themselves -
http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
Another alternative is to add on child views to a property of the
parent view and manually clean them up when the parent view state is
removed.
var ParentView = Backbone.View.extend({
initialize : function(){
this.childViews = [];
},
render: function(){
this.childViews.push(new ChildView);
}
});
A third alternative is to make the child views subscribe to events
that the parent views trigger, so that they can clean up when the
parent view publishes a "close" event.
Also I noticed from your code that you are actually fetching a model within your child view class. Ideally, I would suggest passing the model as a parameter to the constructor as this decouples the view from the data. It's more MVC-ish