Backbone.js: Routing for nested views - javascript

I'm trying to figure out following scenario:
Lets say that I have two views: one for viewing items and one for buying them. The catch is that buying view is a sub view for viewing.
For routing I have:
var MyRouter = Backbone.Router.extend({
routes: {
'item/:id': 'viewRoute',
'item/:id/buy': 'buyRoute'
}
});
var router = new MyRouter;
router.on("route:viewRoute", function() {
// initialize main view
App.mainview = new ViewItemView();
});
router.on("route:buyRoute", function() {
// initialize sub view
App.subview = new BuyItemView();
});
Now if user refreshes the page and buyRoute gets triggered but now there is no main view. What would be best solution to handle this?

I am supposed that the problem you are having right now is that you don't want to show some of the stuff inside ViewItem inside BuyView? If so then you should modularized what BuyView and ViewItem have in common into another View then initialize it on both of those routes.
Here is a code example from one of my apps
https://github.com/QuynhNguyen/Team-Collaboration/blob/master/app/scripts/routes/app-router.coffee
As you can see, I modularized out the sidebar since it can be shared among many views. I did that so that it can be reused and won't cause any conflicts.

You could just check for the existence of the main view and create/open it if it doesn't already exist.
I usually create (but don't open) the major views of my app on booting up the app, and then some kind of view manager for opening/closing. For small projects, I just attach my views to a views property of my app object, so that they are all in one place, accessible as views.mainView, views.anotherView, etc.
I also extend Backbone.View with two methods: open and close that not only appends/removes a view to/from the DOM but also sets an isOpen flag on the view.
With this, you can check to see if a needed view is already open, then open it if not, like so:
if (!app.views.mainView.isOpen) {
//
}
An optional addition would be to create a method on your app called clearViews that clears any open views, perhaps with the exception of names of views passed in as a parameter to clearViews. So if you have a navbar view that you don't want to clear out on some routes, you can just call app.clearViews('topNav') and all views except views.topNav will get closed.
check out this gist for the code for all of this: https://gist.github.com/4597606

Related

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.

Backbone Collection get(id) method

I have one main home page in my application and another page for each post that can be accessed through a list displayed in the home page..
this is how my router looks like :
var AppRouter = Backbone.Router.extend({
initialize: function(){
this.model = new model();
this.collection = new collection();
},
routes: {
"" : "showForm",
"post/:id" : "showPost"
},
showPost: function(id){
var curmodel = this.collection.get(id);
var post = new postView({model:curmodel});
post.render();
$(".maincontainer").html(post.el);
},
showForm : function(){
var qcView = new qcV({model:this.model, collection:this.collection});
qcView.render()
$(".maincontainer").html(qcView.el);
}
});
this is what one of the links to the posts in this list looks like
<h2><a id= "<%=_id%>" href="#post/<%=_id%>"><%=name%></h2></a>
my first question is: Is it dangerous to link pages with a hash-based URL in this manner?
my second question is: I am having no problem navigating to a posts view if I click one of the links in my home page. I my url successfully changes to something like http://127.0.0.1:3000/#post/51ffdb93c29eb6cc17000034 and that specific post's view is rendered. However at that point if I refresh the page, or if I directly type http://127.0.0.1:3000/#post/51ffdb93c29eb6cc17000034to my URL bar the this.collection.get(id) method in my showPost method in the router returns undefined. Can anyone help me figure out why this is the case?
I checked couple times that my initialize method gets called both times, and my collection and model is created successfully
For #2, you are most likely not fetching the collection on the "post" route. Try fetching the collection (if it does not exist) and then call render. That should do the trick!
I think #Trunal's on the right path for the 2nd question. For the first, no, it's not "dangerous". You're not really doing anything different than you would with a classic server-side app, passing information to the server via GET to retrieve info. In my opinion, this should be the preferred approach to implementing routes (rather than attempting to trigger backbone.history.navigate manually, as it avoids all kinds of setup and eventing issues that might otherwise occur).

Parent - child application model; Loading whole applications in a container

How do I load a 'child' application inside a 'parent' application?
I have this main application called the Frame and several child applications. The frame has a border layout. On the left there are some buttons (like a menu) to load other projects. In the center there is the container for my child projects. The frame project has the default mvc structure.
Inside the folder I put a Test application. Also the default structure.
/frame/app/controller
/view
/test/app/controller
/store
/model
/view
/css
/app.html
/app.js
/test2/app/controller
/store
/model
/view
/css
/app.html
/app.js
/css
/app.html
/app.js
something like this.
Now I wanted to load from a button in the frame a child project.
So my function would look something like this:
function(){
Ext.require('Test.view.testMainContainer', function(){
var toPutInMyContainer = Ext.create('Test.view.testMainContainer');
console.log(toPutInMyContainer);
});
}
With this code are 2 things wrong:
the js is in test/app/view/testMainContainer.js
the function never fires...
What is the best structure an how should I approach this?
What i want to do next is inside the testMainContainer are requires for controllers, models, stores and views and I want to load them automatically when needed.
There are various strategies to achieve this.
With respect to the title question, you probably want to have a look at the SubAppDemo by Mitchell Simoens, which demonstrates how to load an sub application within an application.
A similar, yet different, approach is to dynamically load controller upon request. Here is my code to do (part of the application object):
loadPage: function(aControllerName)
{
var iController = this.dynamicallyLoadController( aControllerName ),
iPage = iController.view,
iContentPanel = this.getContentPanel(),
iPageIndex = Ext.Array.indexOf(iContentPanel.items, iPage);
// If the page was not added to the panel, add it.
if ( iPageIndex == -1 )
iContentPanel.add( iPage );
// Select the current active page
iContentPanel.getLayout().setActiveItem( iPage );
},
dynamicallyLoadController: function(aControllerName)
{
// See if the controller was already loaded
var iController = this.controllers.get(aControllerName);
// If the controller was never loaded before
if ( !iController )
{
// Dynamically load the controller
var iController = this.getController(aControllerName);
// Manually initialise it
iController.init();
}
return iController;
},
When the controller is loaded dynamically, all its models and stores are also loaded dynamically. In my case, I always explicitly create the first view of the controller (which means the view is also dynamically loaded) and inject it into the controller's view property (controller code):
init: function()
{
this.callParent();
// The dynamically created view is stored as a property
this.view = this.getView(this.views[0]).create();
},
With regards to your code, I'm not sure why your function will fire in the first place. But it should work if you put the Ext.require outside any function.

Backbone.js : Structuring Application for fixed side panels

I have an application that has a middle panel that always changes depending on what part of the application the user is looking at. These might be messages, transactions etc.
Then there are 4 'fixed' panels at the 4 corners of the application around the middle panel that are mostly fixed for the lifetime of the application, but contain dynamically updated data and therefore need to be implemented using backbone.js
How do I structure such an application in backbone.js. It seems to defeat the "Do not repeat" rule to implement the intial rendering for all the side panels within every route in the router as I would end up repeating the same rendering code in every route.
How do I structure my code in this instance so that I don't repeat code in multiple places.
JavaScript is like any other code: if you find yourself writing the same lines of code, extract them in to a function. If you find yourself needing to use the same function, extract it (and related functions and data) in to its own object.
So, your router shouldn't be calling your views and models directly. Instead, it should be delegating to other objects that can manipulate your views and objects.
Additionally, since your going to set up the same basic page layout every time the app starts up, you might not want that code in the router. The layout happens whether or not the router fires, and no matter which route is fired. Sometimes it's easier to put the layout code in another object, as well, and have the layout put in place before the router fires up.
MyApplication = {
layout: function(){
var v1 = new View1();
v1.render();
$("something").html(v1.el);
var v2 = new View2();
v2.render();
$("#another").html(v2.el);
},
doSomething: function(value){
// do someething with the value
// render another view, here
var v3 = new View3();
v3.render();
$("#whatever").html(v3.el);
}
}
MyRouter = Backbone.Router.extend({
routes: {
"some/route/:value": "someRoute"
},
someRoute: function(value){
MyApplication.doSomething(value);
}
});
// start it up
MyApplication.layout();
new MyRouter();
Backbone.history.start();
I've written a handful of articles relating to these things, which you might find useful:
http://lostechies.com/derickbailey/2012/02/06/3-stages-of-a-backbone-applications-startup/
http://lostechies.com/derickbailey/2011/08/30/dont-limit-your-backbone-apps-to-backbone-constructs/
http://lostechies.com/derickbailey/2011/12/27/the-responsibilities-of-the-various-pieces-of-backbone-js/
http://lostechies.com/derickbailey/2012/03/22/managing-layouts-and-nested-views-with-backbone-marionette/

Backbone showing/hiding rendered views best practices

New to using Backbone and have a very simple application. Basically there are Clients and ClientItems. I have a view to show all Clients and if you click on a Client you get taken to their ClientItems. Going to this ClientItems view should just hide the Clients view and going back to Clients should hide ClientItems. Now, in my render() function for each view, it is going through the collections and dynamically adding stuff to the page. When I go back and forth between the two (using the back button) I don't really need to fully render again as all the data is there in the page, just hidden. Where should this logic go? Right now I have it in the render() function but it feels sloppy, what is the preferred way of handling this?
We are using a global variable App with several common function used across application:
var App = {
initialize : function() {
App.views = {
clientView : new ClientsView(),
clientItemView : new ClientsItemsView()
}
},
showView: function(view){
if(App.views.current != undefined){
$(App.views.current.el).hide();
}
App.views.current = view;
$(App.views.current.el).show();
},
...
}
And then I use this App from other parts of application:
App.showView(App.views.clientView);
IntoTheVoid's solution is good – it's nice to have a single place to hide/show views. But how do you activate the logic?
In my experience, routers are the best place for this. When a route changes and the appropriate function is called, you should update the active, visible view(s).
What if you need multiple views to be visible at once? If you have a primary view that always changes when the route changes, and multiple subsidiary sticky views, you need not worry. But if it's more complex than that, think of creating a ComboView that neatly packages all the relevant views into one containing el node. That way the above logic still works, and your router functions are not littered with logic for managing what views are visible at the moment.

Categories