I write an application which consists from nested tabs. As an example I took https://github.com/tbranyen/backbone-boilerplate. I also use RequireJS.
I have at least two levels of nested tabs. Some of them have similar structure and I can reuse their views.
For example, I have the tab named "Articles" which contains the tabs: "New", "Changed", "Deleted" (in practise the number of tabs is much more). I have about the following routes:
/articles
/articles/new
/articles/changed/
/articles/deleted
These routes maps to the method named "showPanel" which takes two arguments: panel and subPanel. Using these arguments I want to show needed panel.
Is there any pattern which I can apply to resolve this issue or any other examples? Thanks!
look at this, hope to be able to help you :)
showPanel method is a general method, show panel and subPanel:
function showPanel(panel, subPanel){
$('#panels').children().hide().filter(panel).show().children().hide().filter(subPanel).show();
}
routes config:
var Router = Backbone.Router.extend({
routes: {
'articles': 'articlesPanel',
'articles/:subPanel': 'articlesSubPanel'
},
articlesPanel: function(){
showPanel('#articles', '');
},
articlesSubPanel: function(subPanel){
showPanel('#articles', '#'+subPanel);
}
});
Related
Hi I'm trying to create a nested route inside an already nested route. In synthesis, I have a portal which contains my sidebar and some other fancy stuff and there I can navigate to clusters. From clusters I should be able to select one cluster and view its details.
My "grandparent" route is called "portal" and the parent "clusters". Here's the code for clusters-route
(function () {
'use strict';
angular.module('ap.clusters.RouteConfig', ['blocks.router','ncy-angular-breadcrumb', 'ap.clusters.ClusterDetailsCtrl'])
.run(onRun);
/* #ngInject */
function onRun(routerHelper) {
routerHelper.configureStates(getStates());
}
function getStates() {
return [
{
state: 'portal.clusters',
config: {
url: '/clusters',
templateUrl: '/views/clusters/clusters.html',
controller: 'clustersCtrl',
controllerAs: 'vm',
title: 'Clusters',
ncyBreadcrumb: {
label: 'Clusters'
},
settings: {
nav: 4,
content: '<i class="fa fa-cubes nav-icon"></i>a<span>Clusters</span>'
}
}
}
];
}
})();
Here's the nested route I'm trying to include:
(function () {
'use strict';
angular.module('ap.clusters.clusterDetailsRouteConfig', ['blocks.router', 'ncy-angular-breadcrumb', 'ap.clusters.ClusterDetailsCtrl'])
.run(onRun);
/* #ngInject */
function onRun(routerHelper) {
routerHelper.configureStates(getStates());
}
function getStates() {
return [
{
state: 'portal.clusters.cluster-details',
config: {
url: '/cluster/:id',
templateUrl: '/views/clusters-details/clusters-details.html',
controller: 'clusterDetailsCtrl',
controllerAs: 'vm',
title: 'Cluster',
ncyBreadcrumb: {
label: '{{vm.cluster.name}}'
}
}
}
];
}
})();
The routing seems to be working fine, since the url is showing ....portal/clusters/cluster/0 or whatever index I select, but the html is not rendering. When I click the link just the url changes. Here's how i call the routing from clusters view (using jade)
a(ui-sref="portal.clusters.cluster-details({id: $index})")
Don't really know whats wrong with it, why isn't the html showing
I recently inherited this project that anram posted the question about. The issue he describes actually arose due to the nature of the site. The site actually has some fairly complex routing and breadcrumb needs (complex from a development standpoint, rather simple and strait forward from a usability standpoint). For more details on that, and a breadcrumb generation issue I am having with this same project, please read my question here.
In the specific case that anram was asking about, we have a portal layout with a banner bar, sidebar menu, table listing views, and detail views. Every one of the listing views can be navigated to directly from the sidebar menu. The detail views can all be navigated to via two means, either from the listing views reachable via the sidebar menu, or by child listing views from some virtual "parent" detail view. There are a number of different levels in this virtual hierarchy, so the navigation paths to get down to the lowest leaf level can be quite short, or quite long, depending on exactly how you navigate.
The problem that anram encountered was due to the fact that all detail views were originally configured to be child states of the listing states in ui-router, when in actuality they were not. They were siblings in the literal sense. A detail view was not a child view embedded in a <div ui-view /> somewhere on the listing views...they were entirely separate views. A such, the name portal.clusters.cluster-detail was incorrect. It implied that the clusters state should be loaded first, and that cluster-detail would be loaded into a ui-view somewhere in that view. There was no ui-view anywhere in the clusters view, so cluster-details did not load. The navigation via ui-router was working, and working as described...the problem wasn't ui-router, the problem was the simple fact that there was no second level of nested state.
I recently refactored the ui-router states, making all detail views direct children of the portal root view, so we now have portal.clusterDetails instead of portal.clusters.cluster-detail. That solved one problem, the detail views now load properly, and we can route to them from any other view (since pretty much every view is now a sibling, and all are children of portal.) This has given rise to another issue, however, regarding breadcrumb generation (see my linked question above if you have more interest in reading about that.)
I created working plunker here.
I would say, that your setting seems to be ok. The only (hidden here) place, which could be wrong is the content of the
templateUrl: '/views/clusters/clusters.html',
Inside of the state: 'portal.clusters'. What could be wrong there?
This is in fact a parent view for state: 'portal.clusters.cluster-details'. And that means, that we need to insert an anchor (ui-view) there:
...
<div ui-view=""></div>
That allows our child to be rendered
Check it in action here
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
I am using backbone.js to create a single page app. I am new to backbone, so please forgive any wrong semantics.
My Problem is when rendering the views.
Initially, I have a javascript in my index.html that executes the some dom manipulation(image slider).
The JS is wrapped in $(window).load() so all is fine on initiation.
The code obviously doesn't execute unless the page is loaded from url. the code will not run from backbone views or router. So the page loads without the dom manipulation.
I have tried to insert my code into the render and initialize function in the view, but to no avail. Should I add this code to the router? that seems to be a bit of a hack.
Where should I include the "dom ready" code?
and / or is there a better way to manage views and their dom elements on load in backbone?
the code:
home.js
window.HomeView = Backbone.View.extend({
initialize:function () {
this.render();
},
render:function () {
$(this.el).html(this.template());
this.startOrbits();
return this;
},
startOrbits:function(){
$(window).load(function() {
$('#orbit-main').orbit({ fluid: '16x6', swipe:true });
$('#orbit-news').orbit({ fluid: '8x6', bullets: true, directionalNav:false, captions:true, advanceSpeed: 9000});
});
},
});
But when I go to another view, then back, the code obviously doesn't
excite
I'm not quite sure what that means. Leaving the "excite" part aside, you don't "go to" views; views are just ways of adding elements to the page, or adding logic to existing elements.
If I had to guess though, I'd imagine that you're using the Backbone router to move between virtual "pages" (and you use views to make those pages). If that's the case, you need to look at the Backbone router events:
http://documentcloud.github.com/backbone/#Router
http://documentcloud.github.com/backbone/#FAQ-events
Specifically, I think you want to bind an event handler (on your router) to "route:nameOfYourRoute", or just :route" (if you want to trigger your logic on every virtual page load).
Hope that helps, and if my guesses are wrong please edit your question to clarify.
I was able to find a solution.
After commenting out the if statement in my router function, things went smoothly.
home: function () {
// if (!this.homeView) {
this.homeView = new HomeView();
// }
$('#main-content').html(this.homeView.el);
this.homeView.startOrbits();
this.headerView.selectMenuItem('home');
},
I do realize that this means I create a new view on every rout trigger.
Please feel free to offer more optimal solutions.
If I created a standard webapp with 3 different "states": index, view, edit, would I create a single controller that had 3 different routes?
Assume the index has 4 Backbone.Views associated with it and edit has 3. If one navigates from index to edit: is it standard to change the appearance of the page by simply removing all the index Backbone.Views and rendering the edit ones? That seems like a lot of work - would it be wise to just toggle "display:none" when moving between modes? If so would that functionality be within the Controller's functions?
For instance would the following be a valid way of doing it?
window.MyController = Backbone.Controller.extend({
routes: {
'#index':index,
'#view/:id':view,
'#edit/:id':edit
},
switchState: function(state){
hideStates();
showState(state);
},
index: function(){
switchState(index)
},
view: function(id){
switchState('view')
},
edit: function(id){
switchState('edit')
}
})
That's exactly what you should do. Re-rendering on is a bad idea, because you would have to remember states (ie. user typed something into a field in edit tab, then switched to index - when he's back to edit, the field would be empty). It's also faster to base navigation on simply hiding&showing appropriate layers.
I've got a backbone.js application that defines two controllers, and the controllers both define route patterns which match the location.hash. I'm having trouble getting both of them to fire - e.g.
ManagerController = Backbone.Controller.extend({
routes: {
":name": "doStuff"
},
doStuff : function(name) {
console.log("doStuff called...");
}
});
Component1Controller = Backbone.Controller.extend({
routes: {
"xyz123": "doMoreStuff"
},
doMoreStuff : function() {
console.log("doMoreStuff called...");
}
});
so if the url is "http://mysite.com/#xyz123", then I am seeing 'doStuff()' called, or if I comment out that route, then 'doMoreStuff()' is called. But not both.
I'm using this architecture because my page is highly component oriented, and each component defines its own Controller. A 'component manager' also defines a Controller which does some house keeping on all routes.
Should I be able to configure two controllers that both respond to the same route? Cheers,
Colin
Short answer: No, you can't do that. One Controller per page.
Long answer: When you instantiate a new Controller, it adds its routes to the History singleton. The History singleton is monitoring the hash component of the URL, and when the hash changes, it scans the routes for the first expression that matches its needs. It then fires the function associated with that route (that function has been bound to the controller in which it was declared). It will only fire once, and if there is a conflict the order in which it fires is formally indeterminate. (In practice it's probably deterministic.)
Philosophical answer: The controller is a "view" object which affects the presentation of the whole page based on the hash component of the URL. Its purpose is to provide bookmark-capable URLs that the user can reach in the future, so that when he goes to a URL he can start from a pre-selected view among many. From your description, it sounds like you're manipulating this publicly exposed, manually addressable item to manipulate different parts of your viewport, while leaving others alone. That's not how it works.
One of the nice things about Backbone is that if you pass it a route that's already a regular expression, it will use it as-is. So if you're trying to use the controller to create a bookmarkable description of the layout (component 1 in the upper right hand corner in display mode "A", component 2 in the upper left corner in display mode "B", etc) I can suggest a number of alternatives-- allocate each one a namespace in the hash part of the URL, and create routes that ignore the rest, i.e.
routes: {
new RegExp('^([^\/]*)/.*$'): 'doComponent1stuff',
new RegExp('^[^\/]*/([^\/]*)\/.*$': 'doComponent2stuff',
}
See how the first uses only items after the first slash, the second after the second slash, etc. You can encode your magic entirely how you want.
I suggest, though, that if you're going to be doing something with the look and feel of the components, and you want that to be reasonably persistent, that you look into the views getting and setting their cookies from some local store; if they're small enough, cookies will be enough.
I have a very similar issue. At present, backbone stops after the first matching route. I have a dirty workaround where I am overriding the loadUrl method of Backbone History. Here I am iterating through all of the registered routes and triggering callback for all of the matching routes .
_.extend(Backbone.History.prototype, {
loadUrl : function() {
var fragment = this.fragment = this.getFragment();
var matched = false;
_.each(this.handlers, function(handler) {
if (handler.route.test(fragment)) {
handler.callback(fragment);
matched = true;
}
});
return matched;
}
})
Philosophically, I am fine with having single controller per page. However, in a component based view framework, it will be nice to have multiple views per route rendering different parts of a view state.
Comments are welcome.
I've used namespacing to deal with a similar problem. Each module comes with it's own module controller, but is restricted to handle routes that start with /moduleName/ this way modules can be developed independently.
I haven't fully tested this yet, if you take a look at the Backbone.js source, you can see this at line 1449:
// Attempt to load the current URL fragment. If a route succeeds with a
// match, returns `true`. If no defined routes matches the fragment,
// returns `false`.
loadUrl: function(fragment) {
fragment = this.fragment = this.getFragment(fragment);
return _.any(this.handlers, function(handler) {
if (handler.route.test(fragment)) {
handler.callback(fragment);
return true;
}
});
}
The any method will stop as soon as it matches a handler route (with the "return true"), just comment the return and the short-circuit will never happend, and all the handlers will be tested. Tested this with a marionette app with two modules, each one having it's own router and controller, listening same routes anb both fired up.
I think this is the simplest way of resolving it
routes: {
'': 'userGrid',
'users': 'userGrid',
}