Destroy Backbone views on route change - javascript

My view should be destroyed after the current route position is left.
So in this schematic example the login view should be destroyed after the user entered his credentials:
I tried to solve this by using Backbone.Router events:
var Router = Backbone.Router.extend({
initialize: function () {
Backbone.history.start();
},
routes: {
"sample" : "sample"
},
sample: function(){
// Build view
var demoView = $("<div/>")
.appendTo(document.body)
.text("I am lost!");
// Destroy view
this.once('route', function(){
demoView.remove();
});
},
});
Unfortunately this does not work as the route events are raised after the routes are executed:
http://jsfiddle.net/hcuX9/
Is there a solution to destroy views after leaving the route position?
Do I have to hack a new event into Backbone.js?

What I use to do is to have an App.current variable pointing to the current view being rendered.
At the top of each route (or the relevant ones in your case), I remove the current view from App.current and then assign it the new view:
someRoute: function() {
if(App.current && App.current.remove) App.current.remove();
// Asign a new current page
App.current = new SomeView();
...
}
That way I only let one view live per route, getting rid of problems like yours.
If you don't like to be checking for App.current and invoking the remove method at the top of every route, you can listen for Backbone.history route event and injecting that logic there:
Backbone.history.on('route', function() {
if(App.current && App.current.remove) App.current.remove();
});

I think you are stuck with your hack, unless you can adapt .listenTo to your needs - then you will need to fire a custom event with .trigger anywhere you have a route change, which might not be possible. Note that this functionality has been requested (and denied) before in backbone:
https://github.com/documentcloud/backbone/pull/494
See that pull request for other patches that try to do the same thing you are doing.

Here, we're using on and off to listen for route events coming in instead of once because we can't rely on a single event not being the current route. When we receive a route even that is not our current route, we can destroy the view and remove the listener:
// Destroy view
var self = this;
var onRoute = function(route, params){
if(route !== 'sample'){
demoView.remove();
self.off('route', onRoute);
}
};
this.on('route', onRoute);
I've modified your test fiddle here: http://jsfiddle.net/rgthree/hcuX9/3/
Another option, as your fiddle (not in your question) navigates directly to another view. This causes the other route's event to fire after the sample2 route. Because of this the above will remove the view. Now, it's much more complete. A hackier way you could handle it is to simply defer the once in a setTimeout so it doesn't listen until after the current route has been fired:
// Destroy view
var self = this;
setTimeout(function(){
self.once('route', function(){
demoView.remove();
});
}, 0);
You can see your fiddle with this method here: http://jsfiddle.net/rgthree/hcuX9/4/

Related

after render / didInsertElement only fires on first transition [duplicate]

Is there any event fired stating the transition/rendering has completed (and the dom is visible/ready).
setupcontroller/activate are before the dom is built/rendered
didInsertElement gets fired only the first time when I've already inserted an element and I'm just switching the model out underneath it.
What I'm really looking for is the transition is complete event
I guess I can do this, but I was kind of hoping it was already built in...
Ember.Router.reopen({
didTransition:function(infos) {
this._super(infos);
console.log('transition complete');
}
});
Even cooler would be a callback to the route that the transition completed for it, I may have to write this and submit a pull request.
There are a couple of different ways you can solve this
didInsertElement
This is fired when the view is inserted on the first time, but not fired if the model is switched out under the view (because Ember likes to reuse items, since it's cheaper than rebuilding the entire DOM). Example below.
Simple
If you only need to do it once, the first time the view is inserted, use didInsertElement
App.FooView = Em.View.extend({
setupSomething: function(){
console.log('the dom is in place, manipulate');
}.on('didInsertElement')
});
Example: http://emberjs.jsbin.com/wuxemo/1/edit
Complex
If you need to schedule something after the DOM has been rendered from the route itself, you can use schedule and insert it into the afterRender queue.
App.FooRoute = Em.Route.extend({
setupController: function(controller, model){
this._super('controller', model);
Ember.run.schedule('afterRender', this, function () {
//Do it here
});
}
});
Example: http://emberjs.jsbin.com/wuxemo/2/edit
Transition promise
The transition's promise will complete before it's finished rendering items. But it gives you a hook for when it's done with fetching all of the models and controllers and hooking them up.
If you want to hook up to the transition event you can do it like so:
var self = this;
transitionTo('foo').then(function(){
Ember.run.schedule('afterRender', self, function () {
//Do it here
});
})
The afterModel hook might work for you:
App.MyRoute = Ember.Route.extend({
afterModel: function(model, transition) {
transition.then(function() {
// Done transitioning
});
}
});
I tested this using RC-7 with routes that both do and don't have dynamic segments (i.e., a route with a model and a route without a model). It seems to work either way.
See this JSBin for an RC-6 example:
Output: http://jsbin.com/OteC/1/
Source: http://jsbin.com/OteC/1/edit?html,js
setupController is the last thing that the Router calls before finalizing the transition. And if it completes without errors, as far as Ember is concerned the transition is complete. You actually see this in action by enabling LOG_TRANSITIONS_INTERNAL.
At that point, It doesn't matter if the controller has thrown an error, view has thrown an error, etc. The router has completed transitioning into the target route.
So setupController is the last place in terms of the Router that corresponds to didTransition.
When the content/model backing the controller changes on an existing View, the bindings kick in. Most of the changes that happen to the view at that point are via Metamorphing.
The closest place I can think of to hook into would be View.render which pushes changes into the RenderBuffer. But you still need to account for Metamorphing via mixins that happens here.
didTransition does exist as you hoped -- but its an action and not a hook
XXRouter
actions: {
didTransition: function() {
this.controller.set("hasTransitioned", true); // or whatever is needed?!
return true; // Bubble the didTransition event
},
}
XXController
observeTransition: function() {
alert('complete Transition');
}.observes('hasTransitioned'),

Maintain a stack of Marionette ItemViews within a Marionette Layout

I would like to know if it possible to extend in some way the mechanism Marionette Layouts are based on creating a sort of stack like navigation.
Marionette behaviour.
Before a region show()'s a view it calls close() on the currently displayed view. close() acts as the view's destructor, unbinding all events, rendering it useless and allowing the garbage collector to dispose of it.
My scenario.
Suppose I have a sort of navigation mechanism where a Layout acts as controller and first displays an ItemView called A, then a click somewhere allows to switch to ItemView B. At this point, an action on B (like for example a tap on a back button) allows to return to A without recreating it.
How is it possible to achieve the previous scenario without creating again A and maintaning its state?
For iOS people, I would like to mimic a sort of UINavigationController.
Any advice?
EDIT
My goal is to restore a prev cached view with its state without creating it again.
My scenario is the following. I have a layout with two regions: A e B.
I do a click somehere within A and A and B are closed to show C and D. Now a back click would restore A and B with their states. Events, models, etc...but since views are closed events are removed.
Use a backbone router to listen to URL change events. Setup routes for each of your views and then have the router call the layout to change the view it's displaying in response to each route. The user could click back or forward any number of times and the app responds accordingly and displays the correct view. Your router might look like:
var Router = Backbone.router.extend({
routes: {
'my/route/itemViewA': 'showItemViewA',
'my/route/itemViewB': 'showItemViewB'
},
showItemViewA: function () {
layout.showItemView('a');
},
showItemViewB: function () {
layout.showItemView('b');
}
});
Your layout might look something like this:
var Layout = Backbone.Marionette.Layout.extend({
regions: {
someRegion: 'my-region-jquery-selector'
},
initialize: function () {
this.createViews();
},
createViews: function () {
this.views = {
a: new Backbone.Marionette.ItemView,
b: new Backbone.Marionette.ItemView
};
},
showItemView: function (view) {
this.someRegion.show(this.views[view]);
// You might want to do some other stuff here
// such as call delegateEvents to keep listening
// to models or collections etc. The current view
// will be closed but it won't be garbage collected
// as it's attached to this layout.
}
});
The method of communication between the router and the layout doesn't have to be a direct call. You could trigger further application-wide events or do anything else you can think of. The router above is very basic but gets the job done. You could create a more intelligent router to use a single route with parameters to determine dynamically which itemView to show.
Every time the user does something that requires changing views, you can update the browser's history by using router.navigate('my/route/itemViewB', {trigger: true});. Also, if you set up your app to only render on history change events then you don't need to set up two mechanisms for rending each view.
I use this pattern in my own apps and it works very well.
#Simon's answer is headed in the correct direction. However, the only way to stop Marionette from closing views is to modify a bit of it's Region code.
var NoCloseRegion = Marionette.Region.extend({
open: function(view) {
// Preserve the currentView's events/elements
if (this.currentView) { this.currentView.$el.detach(); }
// Append the new view's el
this.$el.append(view.el);
}
});
The, when be sure to specify our new Region class when creating the Layout view
var Layout = Backbone.Marionette.Layout.extend({
regions: {
someRegion: {
selector: 'my-region-jquery-selector',
regionType: NoCloseRegion
},
},
initialize: function () {
this.createViews();
},
createViews: function () {
this.views = {
a: new Backbone.Marionette.ItemView,
b: new Backbone.Marionette.ItemView
};
},
showItemView: function (name) {
// Don't `show`, because that'll call `close` on the view
var view = this.views[name];
this.someRegion.open(view)
this.someRegion.attachView(view)
}
});
Now, instead of calling show which closes the old view, renders the new, and attaches it to the region (and triggers a few events), we can detach the old view, attach the new, and open it.

How do I initialize events when using Backbone.Marionette.Application?

Marionette is great but some things can get a bit confusing to follow. On my initial page load I show a Marionette.Layout in a region of a Marionette.Application. But for some reason the click events are delegated but are not actually responding. If I navigate to another route (removing the layout view) and then return to the route (re-rendering the view) then the events are active. If I cause the view to be rendered twice in a row, then it works. Maybe I'm initializing this system wrong.
var ListView = Backbone.Marionette.Layout.extend({
el: "#listView", // exists on DOM already
events: {
// this is what is supposed to work
// as well as events in my subviews
"click" : function() { console.log("clicked");}
});
var Router = Backbone.Router.extend({
initialize: function(options) {
this.app = options.app;
},
start: function(page) {
console.log("start...");
// with silent false this causes list() to be called
// and the view is rendered
// but the ListView does not get its events delegated
Backbone.history.start({
pushState: false,
silent: false
});
// only by running loadUrl here
// do I get the events delegated
// or by switching between other routes
// and coming back to 'list'
// but this is causing a second useless calling
// of list() re-rendering
console.log("loadUrl...");
Backbone.history.loadUrl(page);
// actually the events are not yet delegated at this point
// if I pause a debugger here.
// events only active after the Application finishes its initializers
},
routes: {
'list': 'list'
},
list: function() {
var lv = ListView({});
this.app.focus.show(lv)
}
});
var app = new Backbone.Marionette.Application();
app.addInitializer(function(options) {
this.router = new Router({app: this});
this.router.start();
});
app.addRegions({
// #focus does exist on the DOM
focus: "#focus",
});
app.start({});
An alterative start that I expected would work but sadly doesn't
start: function(page) {
console.log("start...");
Backbone.history.start({
pushState: false,
silent: true // don't trigger the router
});
// call the router here via history
console.log("loadUrl...");
Backbone.history.loadUrl(page);
// causes page to be rendered correctly
// but events are not delegated
},
I've stepped through it all with a debugger but still can't see what the magical effect of loadUrl is that causes it to work.
I've also tried just reinserting the layout into the region:
app.focus.show(app.focus.currentView)
Maybe there's a different way to structure this that would simply not run into this issue.
Not 100% sure this is the answer, but...
If you're elements exist on the DOM already, and you are trying to attach a view to them, and attach that view to a region, using the region's attach method:
list: function() {
var lv = ListView({});
this.app.focus.attach(lv)
}
the attach method was created specifically for the situation where you want to use an existing DOM element. Calling show instead, will replace what's there, which may be causing the jQuery events to be released.
in the onRender section add
this.delegateEvents();
When Marionette closes a view it unbinds events as well. Events are only delegated when the view is initialized. When you show a previously stored view you will need to call the delegateEvents method again.
https://github.com/marionettejs/backbone.marionette/issues/714

Why is the click event triggered several times in my Backbone app?

I'm making a Backbone.js app and it includes an index view and several subviews based on id. All of the views have been bound with mousedown and mouseup events. But every time I go from a subview to the index view and then go to any of subviews again, the mousedown and mouseup events in the current subview will be triggered one more time, which means when I click the subview, there will be several consecutive mousedown events triggered followed by several consecutive mouseup events triggered.
After looking through my code, I finally found that it's the router that causes this problem. Part of my code is as follows:
routes: {
"": "index",
"category/:id": "hashcategory"
},
initialize: function(options){
this._categories = new CategoriesCollection();
this._index = new CategoriesView({collection: this._categories});
},
index: function(){
this._categories.fetch();
},
hashcategory: function(id){
this._todos = new TodosCollection();
this._subtodolist = new TodosView({ collection: this._todos,
id: id
});
this._todos.fetch();
}
As you can see, I create the index collection and view in the initialize method of the router, but I create the subview collection and view in the corresponding route function of the router. And I tried to put the index collection and view in the index function and the click event in index view will behave the same way as subviews. So I think that's why the mousedown and mouseup will be triggered several times.
But the problem is, I have to use the id as one of the parameters sent to subview. So I can't create subview in the initialize method. What's more, I've already seen someone else's projects based on Backbone and some of them also create sub collection and view in the corresponding route function, but their app runs perfectly. So I don't know what is the root of my problem. Could someone give me some idea on this?
Sounds like you're having a delegate problem because:
all sub views all use the a same <div> element
Backbone views bind to events using jQuery's delegate on their el. If you have a view using a <div> as its el and then you use that same <div> for another view by replacing the contained HTML, then you'll end up with both views attached to that <div> through two different delegate calls. If you swap views again, you'll have three views receiving events through three delegates.
For example, suppose we have this HTML:
<div id="view-goes-here"></div>
and these views:
var V0 = Backbone.View.extend({
events: { 'click button': 'do_things' },
render: function() { this.$el.html('<button>V0 Button</button>'); return this },
do_things: function() { console.log('V0 clicked') }
});
var V1 = Backbone.View.extend({
events: { 'click button': 'do_things' },
render: function() { this.$el.html('<button>V1 Button</button>'); return this },
do_things: function() { console.log(V1 clicked') }
});
and we switch between them with something like this (where which starts at 0 of course):
which = (which + 1) % 2;
var v = which == 0
? new V0({el: $('#view-goes-here') })
: new V1({el: $('#view-goes-here') });
v.render();
Then you'll have the multi-delegate problem I described above and this behavior seems to match the symptoms you're describing.
Here's a demo to make it easy to see: http://jsfiddle.net/ambiguous/AtvWJ/
A quick and easy way around this problem is to call undelegateEvents on the current view before rendering the new one:
which = (which + 1) % 2;
if(current)
current.undelegateEvents(); // This detaches the `delegate` on #view-goes-here
current = which == 0
? new V0({el: $('#view-goes-here') })
: new V1({el: $('#view-goes-here') });
current.render();
Demo: http://jsfiddle.net/ambiguous/HazzN/
A better approach would be to give each view its own distinct el so that everything (including the delegate bindings) would go away when you replaced the HTML. You might end up with a lot of <div><div>real stuff</div></div> structures but that's not worth worrying about.

Order of events triggerd by Backbone.Router when using navigate

I am using Backbone's "all" event to catch all route events in my app in order to log the page views. This works well as long as I don't use navigate to manually trigger a route.
In the following example, I forward the user from the dashboard route to the login route. Backbone fires the event AFTER the route callback is executed, leading to the following output:
showDashboard
showLogin
route:showLogin
tracking:/login
route:showDashboard
tracking:/login
Obviously this is not what I want. I know I could call showLogin instead of using navigate to trigger the login route and this is what I am doing right now, but I would like to know why the order of the events is not the same than the order of the triggered callbacks.
Here is my router (shortened):
var AppRouter = Backbone.Router.extend({
routes: {
"/login": "showLogin",
"": "showDashboard",
},
initialize: function() {
return this.on('all', this.trackPageview);
},
trackPageview: function(eventName) {
console.log(eventName);
var url = Backbone.history.getFragment();
console.log('tracking: ' + url);
},
showDashboard: function() {
console.log('showDashboard');
// check if the user is logged in etc.
this.navigate('#/login', { trigger: true });
},
showLogin: function() {
console.log('showLogin');
}
});
Backbone's Router is actually very simple, and if you read the code you'll see the following in it's constructor:
this._bindRoutes();
this.initialize.apply(this, arguments);
_bindRoutes attaches all your routes as you expect, and it does this before your initialize function gets called. So your binding will always fire after Backbone's does.
You're probably going to be better off finding another way to do this.
You could call a before type function yourself in your routes to do stuff like track pageviews/etc. Or maybe you could just override route, track your pageview and then make sure to call Backbone's implementation with something like Backbone.Router.prototype.route.call(arguments);

Categories