I have a very basic Backbone JS application that has modals. Currently, my router presents the modals as follows:
class App.Routers.Router extends Backbone.Router
routes:
"modal" : "modal"
modal: ->
view = new App.Views.Modal.New()
$('#shared').html(view.el)
view.render()
view.show()
return
class App.Views.Sessions.New extends Backbone.View
template: Handlebars.templates["backbone/templates/modals"]
initialize: (options) ->
super(options)
render: ->
$(#el).html(#template())
$('.modal', #el).modal()
$('.modal', #el).on 'hidden', #cleanup
return #
show: ->
$('.modal', #el).modal('show')
hide: ->
$('.modal', #el).modal('hide')
cleanup: ->
# ?
This works fine, however I am unclear of how to handle the window history and a user selecting the back button (i.e. how to I teardown the modal on clicking back). Does anyone have any ideas on the best approach? Thanks.
You've stumbled on an interesting problem with single page apps (SPAs). Yes it can get a bit tricky but simple software engineering principles/designs can help here. I've dealt with clean up in the following way:
Have a separate class/object responsible for managing view transitions between various 'parts of the application'. For example in my app I have something like this:
var App = {};
//when showing a specific app:
App.showView = function(appToShow){
if(this.currentApp)
currentApp.close();
this.currentApp = appToShow;
//render appToShow;
}
appToShow is a 'class' that has methods create/render and close. So that the app is responsible for it's clean-up.
Now sometimes I have modal or "sub apps" within the main application. I've used a variant of the above and added a close method to the App object. But the basic idea is to add these 'sub apps' as a property of the main app:
//when creating modal:
App.addModal = function(modal){
this.currentApp.modal = modal;
}
Now when transitioning by clicking the back button or to a different part of the app - you must call upon the App-manager to handle the clean-ups and transitions. It's basically like saying:
App.closeModal = function(modal){
if(this.currentApp.modal)
this.currentApp.modal.close();
}
Depending on how your routers are organized, you should be able to decide whether to close the entire app altogether or just the modal/sub-apps. Basically your "App Manager" is a separate object responsible for only handling transitions between the views and need not be a Backbone.View - a separate object would work just fine. You could even have it listen for events using Backbone's events by creating and event aggregator. Derick Bailey has written an excellent blog post detailing the same: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
Related
I am using bootstrap, backbone, marionette in my app with require js support. This is a moderately large application with many views and sub views. As it consists of tabbed display, I am using bootstrap tabs.
In my layout view, I am handling the tab shown event and trying to do tab pane specific rendering as below...
var AppLayoutView = Backbone.Marionette.LayoutView.extend({
el : "body",
regions: {
headerRegion: "#ecp_header",
bodyRegion: "#ecp_body",
contentRegion: "#home"
},
events: {
'shown.bs.tab ul.nav-tabs a': 'onTabShown'
},
...
onTabShown: function(e) {
var self = this;
console.log("App layout view: 'onTabShown' executing...");
var tabId = $(e.currentTarget).attr('id');
if (tabId === 'home-tab') {
/** Show the dashboard (home) view */
require(['user_dashboard_layout'],
function(UserDashboardLayoutView) {
// update the URL in addressbar, so it can be available in history stack
Backbone.history.navigate('dashboard');
var dbLytView = new UserDashboardLayoutView();
dbLytView.render();
//self.bodyRegion.show(dbLytView);
//self.contentRegion.attachView(dbLytView);
});
} else if (tabId == "scheduling-tab") { ... }
All of this was working decently, until I added the line the history navigation line as shown in the above code.
Backbone.history.navigate('dashboard');
I also added app_controller to take care of routing. But after this, I observe a strange behavior. I see that the above fn "onTabShown" gets invoked multiple times, as seen in the browser console window (screenshot attached), whenever I perform login/logout on my app.
BTW, in my SPA (single page app), when user logs in, I show dashboard (if the user is logged in), or show welcome page (if not logged in).
If the offending line (history.navigate(...)) is present, I can see that tabShown is invoked multiple times, that is, for each login/logout it gets accumulated (some sort of strange recursion, or stack is not unwound).
But, if I comment out the history.navigate line, it doesn't perform page refresh after logout.
My basic question is...
"does the backbone.history.navigate(...) fn play any role in actual page navigation/refresh, apart from just updating the history stack?
From the documentation, it appeared that we need to call bb.h.navigate(...) just to keep the url in addressbar in sync with our current state of app. However, I am experiencing this strange behavior?
Since the app is a bit fairly complex, I may not have provided all relevant details for soliciting a proper answer to this qn.
May someone point me in right direction...?
I have skimmed through all the marionette articles on the layout view and I am not sure if there are advantages to using that versus how I have my app setup now, let me show you.
After building the components to my app I then created an app level view that handles the initialization and rending of all those views.
var Backbone = require('backbone'),
TeamsView = require('./teams'),
DataView = require('./teamData'),
LeaderView = require('./leader');
module.exports = appView = Backbone.View.extend({
el: '#wrap',
template: require('../../templates/app.hbs'),
initialize: function() {
window.App.views.teamsView = new TeamsView({ collection: window.App.data.teams });
window.App.views.dataView = new DataView({ collection: window.App.data.teams });
window.App.views.leaderView = new LeaderView({ collection: window.App.data.teams });
},
render: function() {
var teamsView = window.App.views.teamsView;
var dataView = window.App.views.dataView;
var leaderView = window.App.views.leaderView;
this.$el.find('#basketball .app').prepend(teamsView.render().el);
this.$el.find('#basketball .app').prepend(dataView.render().el);
this.$el.prepend(leaderView.render().el);
}
});
Then inside a controller I render the app view above.
This feels comfortable to me, but somewhere deep inside says it's wrong and I should be looking at layout views?
So my question more specifically is when putting the pieces together in an backbone application should I be looking into layout views or is creating a single app level view (like above) sufficient?
What you're doing is fine at the simplest level, though perhaps more manual than it needs to be. However, you may want a Layout View as things get more complex.
The value of a Layout View comes when you have a region in your App that you'd like to have contain sub-regions, but only during certain views. For example, you might have an index/list inside of your App's #mainRegion that just has a bunch of teams, in which case you could use a CollectionView (or CompositeView if you want to add some styling) along with the ItemView for each team.
However, say you click to edit one of the teams, and now you want the #mainRegion to show the edit page, which itself has some information about the team in #infoRegion and then an edit form in a #formRegion. These are regions that are specific to the edit page, and so you'd want to use a Layout View to manage them rather than delegating all the way up to the App level.
I hope this makes sense, and I'm happy to clarify if needed. You can also check out the documentation for a breakdown of when to use each view type: https://github.com/marionettejs/backbone.marionette/wiki/Use-cases-for-the-different-views
From my understanding, the differences is the callback functions to events on an AppRouter should exist in the Controller, instead of the same Router object. Also there is a one-to-one relationship between such AppRouter & Controllers, all my code from Router now moves to Controller, I don't see too much point of that? So why use them? I must be missing something?
The way I see it is to separate concerns:
the controller actually does the work (assembling the data, instanciating the view, displaying them in regions, etc.), and can update the URL to reflect the application's state (e.g. displayed content)
the router simply triggers the controller action based on the URL that has been entered in the address bar
So basically, if you're on your app's starting page, it should work fine without needing any routers: your actions (e.g. clicking on a menu entry) simply fire the various controller actions.
Then, you add on a router saying "if this URL is called, execute this controller action". And within your controller you update the displayed URL with navigate("my_url_goes_here"). Notice you do NOT pass trigger: true.
For more info, check out Derick's blog post http://lostechies.com/derickbailey/2011/08/28/dont-execute-a-backbone-js-route-handler-from-your-code/ (paragraph "The “AHA!” Moment Regarding Router.Navigate’s Second Argument")
I've also covered the topic in more length in the free preview of my book on Marionette. See pages 32-46 here: http://samples.leanpub.com/marionette-gentle-introduction-sample.pdf
I made some override for the router. And currently use it in this way (like Chaplin):
https://gist.github.com/vermilion1/5525972
appRoutes : {
// route : controller#method
'search' : 'search#search'
'*any' : 'common#notFound'
},
initialize : function () {
this.common = new Common();
this.search = new Search();
}
Many of the views in my application need to be "collapsible". To the user this means that you can click an arrow to collapse or expand the view's contents.
When creating a view I need to be able to easily say, "This view should be collapsible," and then run the appropriate setup code (which essentially means adding the .collapsible class to the view's wrapper and inserting a dom element that looks like this: <div class="toggle"></div>
Suggestions on ways to pull this off seamlessly? I'm currently using Backbone, Backbone.Marionette, and Underscore.
I do this with another application that doesn't use Backbone. In that application every action results in a page refresh, so I just use jQuery to look for all elements with the .collapsible class and do my setup that way.
EDIT:
I'm using Backbone.Marionette.CompositeView for these particular views, if that helps.
I've done similar thing in my project by extracting such functionality into mixins. There're different approaches to implementing mixins in Backbone. Take a look here or here
You can create parent view that extends from Marionettes compositeView and add your common functionallity there, and have your project views extend from this parent view.
var CollapsibleView = Backbone.Marionette.CompositeView.extends({
variable1: 1,
var2: true,
initialize : function() {
// your code here
},
helperfunction : function () {
// other helpful function
}
});
var MySpecificView = CollapsibleView.extends({
mySpecificFunction : function () {
// some specificView functionality
}
});
var myProjectView= new MySpecifcView();
myProjectView.helperfunction(); /// function from the parent
myProjectView.mySpecificFunction(); /// function from the specificView
/// you also have the functionality added on the initialization of the collpasibleView
Is there a preferred way to trigger Router.navigate in Backbone when a user clicks a link?
For instance a template might have a link Log Out. Is the preferred approach really to use a custom class and attach a click handler to that class in the view? This seems to generate tons of duplicate code so I'm looking for a better way.
The way to handle this kind of thing is to extend the Backbone objects ( in this case the View). You'll notice in the Backbone docs that this is encouraged, and necessary given its minimalist code base. I'd recommend checking out Marionette (on Github) and the site Backbone patterns for good ways to extend the Backbone core.
For example, maybe you extend View with a method that wires up the navigation handlers as you like:
Backbone.View.prototype.wireupNavs = function() {
var that = this;
this.$el.find("a[role=nav]").each(function() {
var target = $(this).attr('href');
that.bind("click", router.navigate(target);
});
}
Then any a tag you want as a Backbone nav element, you'd just decorate with the role="nav" attribute; and call this.wireupNavs() in the initializer function of the appropriate views.