Backbone.js navigation working once but not again - backbone.js

I'm using Backbone.js 0.9.2 from the Backbone-on-rails gem. I'm also trying to use the new 'pushState' instead of the old hash URL.
The Problem
I'm building a standard Rails like CRUD interface to keep track of my appointments. I have a 'new' link on the main index.jst.eco page:
<h1>Appointments</h1>
<p>New Appointment</p>
I load the page and click on that 'new' link and backbone fires off the event and doesn't have to reload the whole page. Here is that event:
class BackboneOnRails.Views.AppointmentsIndex extends Backbone.View
template: JST['appointments/index'],
events: ->
'click .new': 'newAppointment'
newAppointment: ->
Backbone.history.navigate("/appointments/new", {trigger: true})
return false
# The rest of the index methods omitted for brevity
This then invokes the backbone router:
class BackboneOnRails.Routers.Appointments extends Backbone.Router
routes:
'': 'index'
'appointments': 'index'
'appointments/new': 'new'
initialize: ->
this.appointments = new BackboneOnRails.Collections.Appointments()
this.appointmentsIndexView = new BackboneOnRails.Views.AppointmentsIndex({collection: this.appointments})
this.appointmentsIndexView.render()
index: ->
$("#container").html(this.appointmentsIndexView.el)
this.appointments.fetch()
new: ->
appointments = new BackboneOnRails.Collections.Appointments()
view = new BackboneOnRails.Views.AppointmentNew({collection: appointments})
$("#container").html(view.render().el)
The problem happens when I hit the browsers back button, then try using the 'new' link again. This time around it does a full reload of the page.
What is happening to the javascript bindings when I hit back on the browser?
I have a show event for the item and with that I can go back and forth no problem. I've compared both and they look like the same sort of calls.

The problem is in your attempted re-use of the appointmentsIndexView instance. Removing the view from the DOM destroys the DOM event handlers. Re-adding the view's el to the DOM does not re-connect them.
Overview Of The Problem
When you load that view with the initialize and index methods of your router the first time, everything is fine because you have a fresh instance of the IndexView. The DOM events are attached to the view properly, and life is good.
When you hit the new route / method of your router, you're effectively trying to remove the index view from the screen and replace it with the add new view. This works from a visual stand point and from the standpoint of the add new view.
When you hit the back button, though, you're staying within the same live application instance in your browser tab. Hitting the back button with pushstate enabled tells the browser not to reload the entire app, just to update the url and fire off the router method for the index.
In this case, you're index view is not re-built from the ground up. You're re-using the same view instance, but re-loading it with data from the server. The data load works perfectly fine because your view and collection are still attached. The DOM event bindings fail, however, because they bindings were previously removed and not re-added.
2 Common Solutions
There are two common solutions for this, and many variations of these solutions.
1) Don't re-use view instances.
This is my strongly recommended suggestion. In every instance where I have tried to re-use a view instance, I have consistently run into very large problems - including the exact problem you're having.
Instead, re-create a new view instance every time you need to show the index of appointments. That means you create the new index view in your index method of the router, instead of the initialize method.
2) Clear and re-bind the DOM events
If, for some reason, you feel that you really need to re-use the view instance (which should never be true), you can solve it with some information that Tim Branyen posted on his blog a while back:
http://tbranyen.com/post/missing-jquery-events-while-rendering
I do not recommend this approach. Re-using a view instance might seem like a good idea off-hand, but it will lead down a bad path toward other problems, including bloated memory usage by leaving too many unused parts around in your app.
Side Note: Zombies And Memory Leaks
In either case - whether you decide to re-use view instances or re-create them when you need them - you're likely to run in to some memory leaks.
In the case of re-using a view, you're explicitly holding on to an object in memory when you don't need to. This isn't really a "leak" but it's an excessive use of memory. You should de-reference the object when it's not needed and re-create it when it is needed. This will cut down on memory usage and allow your app to perform better.
I have a blog post covering how this works, here: http://lostechies.com/derickbailey/2012/03/19/backbone-js-and-javascript-garbage-collection/
In the case of not re-using a view, you may wind up with a true memory leak by leaving model and collection event bindings hanging around after a view has been removed from the visible DOM. If you decide not to re-use your views, you will need to make your code that replaces the #container html more robust, and have it clean up the old view.
I've got a blog post detailing a solution for that, as well: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/ - be sure to read the comments from Johnny Oshika in this post, as he points to a very useful StackOverflow answer where he shows a simple method of handling model and collection events.

Related

Backbone - Reusing Views vs Creating new Views

Writing my first Backbone app - came across a predicament wherein i am unable to choose which is the best way to move forward.
Scenario : User clicks an edit button , a new view is loaded . Approach is as below.
renderEditView: function(){
if(my.namespace.view){
my.namespace.view.render();
}else{
my.namespace.view= new editView({model:my.namespace.model});
}
}
Basically, i am assigning my view to a namespaced variable and resuing it as required. Didn't face any problems as such.
But some advocate recreating the View again using new editView({model:xxx}); whenever the edit button is clicked . i Would like to know which one is the better practice and why?
P.S: i am aware of the 'event ghosting' problem in BB apps, and the excellent solution provided by Derick Bailey .But still would love to know the pros and cons between the approaches.
This is indeed an opinion, because either way will work if you (as you mention) take care of cleaning up previous views if you decide to instantiate a new one every time you want to re-render. It's important to avoid duplicating lingering events from every instance that you want to replace by creating a new one.
Personally I have used both strategies and never had problems with them so far.
When re-using a view, I bind the view as a property to the controller object that renders the view, pretty much the same way you do it.
Theoretically, I don't see a reason to re-instantiate a view if it was already created before. It isn't that you really require a new instance, it's just that you want to re-render it.
Sidenote
For re-rendering views, Backbone Marionette offers regions, which are convenience objects that allow you to do things like:
var myView = new MyView();
var region = new Marionette.Region({el: "#container"});
region.show(myView);
In case you would decide to instantiate a new view every time, these regions take care that previously rendered views are properly cleaned up.

is angulars reloadOnSearch evil?

I've been programming with angularjs for some time now, however, i started using reloadOnSearch for my application and at first i thought this was going to save me for uneccesary reloads of controllers and ajax calls against my API.
For example say i have a list of things, and then present them in a view. When you first access this view it fetches the list of items from the API and presents them, and when you click on an item it adds the ?id=xxx&view=show query parameter to the url without reloading the controller.
When we access a single item from the already loaded list, it just fetches the list item from that we already have and presents it. However, if we forcibly reload the page, the controller now realizes that it has to fetch it from the API instead as it doesn't exist within the list collection.
At first this seemed like a great thing. However as i think about it, i now have to manage the state of the entire controller, whereas before i could have state enclosed within single functions in the controller.
I seem to be having trouble deciding if reloadOnSearch is evil and should be avoided, or if it is worth keeping around. What is your opinion?
Also, would it be better to use something like ui-router instead? I just saw a introduction video which implies that one can have better control of state.
I've converted my app to use ui-router instead. which basically nests application logic in "sub scopes" making it possible to share data between states/pages. I realize now that this is possible without ui-router aswell, however one would have to create pages that has a hierarchy of <div ng-controller>
So in conclusion, the way i was using reloadOnSearch was indeed evil. However, there were better ways of using it aswell.

Pagination with collection.fetch() in backbone.marionette

I am trying to implement pagination with backbone.marionette's
compositeView by first doing an initial fetch:
collection.fetch({reset: true})
To load additional models into the same collection,
collection.fetch({reset: false, remove: false})
is used.
I saw that on each use of fetch() with {reset: false}
muliple add events are triggered (for every model added.)
Thats fine basically, but when looking into the annotated source
of the compositeView's appendHtml function in marionette the follwing
statement is given:
buffering happens on reset events and initial renders in order to
reduce the number of inserts into the document, which are expensive.
While this is great for the initial fetch (only 1 browser reflow), every
subsequent fetch with {reset: false} could get pretty expensive
(reflow on every single model thats added). It really seems to
append() the itemView every single time - causing a reflow
(compositeView.isBuffering is false).
Is there any way to "batch" process the models and do a single insert
(same as done on the reset event)?
Does this need an overwrite in the appendHtml implementation
and implement the batch logic by myself?
(I assume you don't want to use Backbone.paginator, for some reason. If that isn't the case, use the plugin ;-D).
You could use a technique similar to the filtered collection used here: https://github.com/davidsulc/marionette-gentle-introduction/blob/master/assets/js/entities/common.js#L2 (instanciation at https://github.com/davidsulc/marionette-gentle-introduction/blob/master/assets/js/apps/contacts/list/list_controller.js#L13).
Basically: you provide a "paginated collection" to the view, and what the paginated collection does is:
initially, return a "cloned" copy of the original collection
when you fetch on the original collection, add the fetched events in "batch" to the cloned collection, which will trigger the view to refresh (because it's listening to the cloned collection, not the original)
To minimize reflows, you can try the following:
silence the events that the collection listens to ("add", etc.)
when a group of models gets added, create a new instance of a collection view with only the new models
render the new collection view (but don't show it anywhere), and manually append the newView.el to the right place in the DOM
destroy the newView
Unfortunately, once you need to start optimizing, you tend to need to handle a bunch of things by hand...

ExtJS4 - Store per panel instance?

I'm really new to the MVC pattern in Ext.
I have a tabpanel with multiple instances of the same component (let's call it product), each should call the server when it's opened, with an id parameter.
Right now, in order to create these tabs - I use this in the Product controller
Which creates a new instance of a view, but I feel like it's really incorrect.
createMainView: function (opts) {
return Ext.widget("productDisplay", opts);
}
I call it from my "main" controller, like this:
var tab = this.application.getController("Products")
.createMainView({ productId : id, closable: true })
tabs.add(tab);
tabs.setActiveTab(tab);
What's the correct way to properly use multiple instances of a view, each having an instance of one store and behavior (via the controller).
Can I use one named store for them (with a js file under app/store/product.js)?
Should I manually call load on the store from the controller (to pass the productId), or is there a nicer way?
It's kind of very broad and interesting question which require big and thorough explanation (which you can find btw on the Sencha.com in their guides and manuals). I would like highlight couple points so you have something to start with:
Stores are usually global objects. You don't keep two instances of one store in general. You can use filtering (local or remote) if you need to present information from the that store in several different views. The only time you would need to clone store is if you want to present different information from that store in 2+ different views at the same time.
Controllers are usually spawned by main application object. You don't have to do anything special - just list them in the controllers: [] property. And then spawed when application is launched (before their views are created and rendered). Keep that in mind.
If you have modal view - it's ok to create it manually and either re-use it or destroy and re-create later. You can add filtering and loading to controller which creates these views. And you can re-use same view/controller objects for different tabs if you want.
If your views are presenting one instance of an object (like one product is displayed on each tab) - don't attach stores to those views. Just pass them individual model (record).
I would recommend creating the stores that relate only to that view instance inside of the view's initComponent method.
Your controller's control handlers should be coded in a way that they can differentiate which view dispatched the event. This should not be too difficult because almost all view events contain a reference to the component which fired the event. You could then use the relative query selectors, e.g.: myEventFiringComponent.up('anotherComponent') or myEventFiringComponent.down('anotherComponent') to get a handle on a different component in the same view if you need to.
Please see this post for a full explanation.

Preferred way of creating links with backbone.js

I'm trying to wrap my head around backbone.js but I'm finding it hard due to the lack of (IMO) good examples.
First of all, what is the best way of getting a link to an object.
If I want to get the edit url of an Album model I could do album.url() + '/edit', is this really the best way?
Also, I'm trying to make my application work 100% without javascript so I don't want my URLs/links to say /albums/#1/edit, I want it to be /albums/1/edit and override this in JS.
I'm thinking I create normal URLs and use jQuery.live to call router.navigate in backbone.js
I never got this to work however, when I call router.navigate('/albums/2', true) the URL changes but my show action is never called. If I refresh it's called so the route is matched.
What am I missing?
The basic answer, which is kind of frustrating, is "there is no preferred way!". Backbone.js doesn't tell you how to set up links, you can do it any way you like. I found this flexibility just as annoying as you do, at least at first.
So here's the way I'm approaching this on my current project, with the (big) caveat that this is just one of many ways to do things in Backbone:
For the most part, I don't use actual links. There's no explicit reason not to, but it means you have to keep track of a bunch of URL strings that have to be consistent. I would rather stick all the URL formatting in my routers and not deal with it elsewhere.
To open a new "top-level" view, like an editing screen, I set something that fires an event. In the application I'm currently working on, I have a global State model, and to open a new view I call state.set({ topview: MyTopView }). This causes the state object to trigger change:topview.
Any piece of the UI that needs to change when the top-level view changes has an update method bound to change:topview. When the event fires, they look at state.get('topview') and update as necessary.
I treat my routers as only marginally specialized parts of the UI - they're essentially views that render in the browser address bar, rather than the window. Like other views, they update the state object on UI events (i.e. a new URL), and like other views, they listen to the state object for changes that cause them to update. The logic that the editing screen has the URL albums/<albumid>/edit is fully encapsulated in the router, and I don't refer to it anywhere else.
This works well for me, but it adds an entirely new pattern, the global State object, to the Backbone structure, so I can hardly call this the "preferred" approach.
Update: Also note that .url(), in the Backbone idiom, refers to the model's URL in the back-end API, not the front-end URL (it's not like Django's get_absolute_url). There is no method in the default Backbone setup that gives you a user-facing URL for your model - you'd have to write this yourself.
Also, I'm trying to make my application work 100% without javascript; so I don't want my URLs/links to say /albums/#1/edit, I want it to be /albums/1/edit and override this in JS.
you can do exactly this w/ pushState. just enable it in your Backbone.history.start call:
Backbone.history.start({pushState: true})
this tells Backbone to use the HTML5 History API (a.k.a. "PushState"), which uses full URLs exactly like you're wanting.
read up on the history api here: http://diveintohtml5.ep.io/history.html
and I wrote up a 2 part series on using pushstate w/ the second part focusing on progressive enhancement in backbone, to do what you're needing:
http://lostechies.com/derickbailey/2011/09/26/seo-and-accessibility-with-html5-pushstate-part-1-introducing-pushstate/
and
http://lostechies.com/derickbailey/2011/09/26/seo-and-accessibility-with-html5-pushstate-part-2-progressive-enhancement-with-backbone-js/
hope that helps :)

Resources