When Backbone Routers is used for rendering subviews into a main view there is an issue I cannot overcome. Same issue when inner view rendering from inside the home view preferred. This is faced when the page is refreshed. I can describe them in detail as follows.
I have a homeview, whole page. A mainrouter directed me to that
homeview at the beginning. Homeview has tabs. Clicking tabs should
show a subview by a method in homeview navigating by mainrouter.
Subviews are whole width under tab bar. After navigated, url has
been updated. Now subview is shown in the place I wrote. If at this
stage one refreshes the page,this same stage is not reached. Because
of the url, directly the router will route to the method to render
the subview but where to put it in dom. Homeview is not there with
its element.
This also should be solved in case when not routers but inner views
are used to render the subviews from click events inside the
homeview, I mean without routes in the main router to create and
call render of the subviews in the main router. Because tab clicks
updates the url. And When one refreshes the page at this point app
knows no where to go. In my case it sometimes refreshes with no
problem and in some cases do not render anything.
Not updating the url in tab clicks can be a solution, but of course click should visit the hashtag it references. Updating the url is not necessary at all since this is even in desktop browser a single page application. So no place to be bookmark-able, back button-able, as Backbone documentation describes for Backbone.Router.
What are the ways to overcome this issue?
Update
http://www.geekdave.com/2012/04/05/module-specific-subroutes-in-backbone/
var MyApp = {};
MyApp.Router = Backbone.Router.extend({
routes: {
// general routes for cross-app functionality
"" : "showGeneralHomepage",
"cart" : "showShoppingCart",
"account" : "showMyAccount",
// module-specific subroutes:
// invoke the proper module and delegate to the module's
// own SubRoute for handling the rest of the URL
"books/*subroute" : "invokeBooksModule",
"movies/*subroute" : "invokeMoviesModule",
"games/*subroute" : "invokeGamesModule",
"music/*subroute" : "invokeMusicModule"
},
invokeBooksModule: function(subroute) {
if (!MyApp.Routers.Books) {
MyApp.Routers.Books = new MyApp.Books.Router("books/");
}
},
invokeMoviesModule: function(subroute) {
if (!MyApp.Routers.Movies) {
MyApp.Routers.Movies = new MyApp.Movies.Router("movies/");
}
},
invokeGamesModule: function(subroute) {
if (!MyApp.Routers.Games) {
MyApp.Routers.Games = new MyApp.Games.Router("games/");
}
}
});
// Actually initialize
new MyApp.Router();
});
MyApp.Books.Router = Backbone.SubRoute.extend({
routes: {
/* matches http://yourserver.org/books */
"" : "showBookstoreHomepage",
/* matches http://yourserver.org/books/search */
"search" : "searchBooks",
/* matches http://yourserver.org/books/view/:bookId */
"view/:bookId" : "viewBookDetail",
},
showBookstoreHomepage: function() {
// ...module-specific code
},
searchBooks: function() {
// ...module-specific code
},
viewBookDetail: function() {
// ...module-specific code
},
});
[OLD]
There are various ways you can do this, the way I prefer is :
var Router = Backbone.Router.extend({
initialize : function(){
app.homeView = new HomeView({el:"body"}); //I prefer calling it ShellView
},
routes : {
"subView/*":"renderS",
},
renderSubViewOne : function(params){
app.homeView.renderSubView('one',params);
}
});
var HomeView = Backbone.View.extend({
renderSubView:function(viewName, params){
switch(viewName){
case 'one':
var subViewOne = new SubViewOne({el:"tab-one"},params); //_.extend will be cleaner
break;
}
}
});
Above code is just an skeleton to give an idea. If the app is complex, I suggest having multiple routers.
Multiple routers vs single router in BackboneJs
Related
Sup! Back button sometimes doesn't work with my polymer project. When i hit back button the page variable is steel the current page and i need to hit the button twice or three times to get it working for example i go to the /#/rules page from /#/home but it doesn't go back to /#/home once i press the back button the second or third time by the way it does go back to the main page. Here is my observer and router:
properties: {
page: {
type: String,
reflectToAttribute: true,
observer: '_pageChanged',
},
},
observers: [
'_routePageChanged(routeData.page)',
],
_routePageChanged: function (page) {
this.page = page || 'home';
this.set('route.path', `/${this.page}`);
},
_pageChanged: function (page) {
// Load page import on demand. Show 404 page if fails
var resolvedPageUrl = this.resolveUrl(page + '.html');
this.importHref(resolvedPageUrl, null, this._showPage404, true);
window.history.pushState({}, null, `#/${this.page}`);
},
And this is my app-route element:
<app-route route="{{route}}" pattern="/:page" data="{{routeData}}" tail="{{subroute}}"></app-route>
Just can't figure out why it does not work the first time. Any help is appreciated and i have already searched a lot with no results.
Can you try this, assuming you have <app-route route="{{route}}"></app-route>?
observers: [
'_routePageChanged(route.path)',
],
_routePageChanged: function(path) {
if (path) {
this.page = this.routeData.page;
} else {
/*
* It's unnecessary to have the following line.
*/
// this.page = 'home';
this.set('route.path', '/home');
}
},
Why it works after all?
I learned my lesson by debugging the source code of <app-route>. If the path is empty, the code for updating data will be skipped - and your observer, _routePageChanged(routeData.page), won't be triggered. See
https://github.com/PolymerElements/app-route/blob/master/app-route.html#L254-L257
https://github.com/PolymerElements/app-route/blob/master/app-route.html#L320-L328
You may consider it to be a flaw in <app-route>. Whatsoever, it's open source, and you can always find your way.
i would like to navigate my nested view in backbone app with routes. I have next code:
var StoreRouter = Backbone.Marionette.AppRouter.extend({
appRoutes: {
'item/:name/' : 'showItem',
"item/:name/species/:speciesName/" : "showSpecies"
}
});
var StoreCtrl = Marionette.Object.extend({
showItem: function(name){
console.log("showItem");
/* execute function for show Item */
},
showSpecies: function(name, speciesName){
console.log("showSpecies");
/* execute function for show Species inside Item Layout */
}
});
So, i need to show species when route is "item/:name/species/:speciesName/" but i get only triggering showSpecies fucntion, not both. What shall i do to trigger showItem and then showSpecies functions when route is "item/:name/species/:speciesName/" ?
There is nothing brand new here. Simply call your showItem function directly from showSpecies.
Additionally, you could use routes hash instead of appRoutes and then it is possible to do so:
var StoreRouter = Backbone.Marionette.AppRouter.extend({
routes: {
'item/:name/' : 'showItem',
'item/:name/species/:speciesName/' : function(){
this.showItem();
this.showSpecies();
}
}
});
I have a question about the way backbone handles it views.
Suppose I have the following code:
<div id="container">
<div id="header">
</div>
</div>
After this I change header into a backbone view.
How can I now remove that view from the header div again after I'm done with the view and add ANOTHER view to the same div?
I tried just overwriting the variable the view was stored in. This results in the view being changed to the new one...but it will have all the event handlers of the old one still attached to it.
Thanks in advance!
http://documentcloud.github.com/backbone/#View-setElement
This won't automatically remove the original div - you'll want to do that yourself somehow, but then by using setElement you'll have the view's element set to whatever you passed it.. and all of the events will be attached as appropriate. Then you'll need to append that element wherever it is that it needs to go.
--- Let's try this again ----
So, first thing to keep in mind is that views reference DOM elements.. they aren't super tightly bound. So, you can work directly with the jquery object under $el.
var containerView = new ContainerView();
var headerView = new HeaderView();
var anotherHeaderView = new AnotherHeaderView();
containerView.$el.append(headerView.$el);
containerView.$el.append(anotherHeaderView.$el);
anotherHeaderView.$el.detach();
containerView.$el.prepend(anotherHeaderView.$el);
Or you can create methods to control this for you.
var ContainerView = Backbone.View.extend({
addView: function (view) {
var el = view;
if(el.$el) { //so you can pass in both dom and backbone views
el = el.$el;
}
this.$el.append(el);
}
});
Maybe setting the views by view order?
var ContainerView = Backbone.View.extend({
initialize: function () {
this.types = {};
},
addView: function (view, type) {
var el = view;
if(el.$el) { //so you can pass in both dom and backbone views
el = el.$el;
}
this.types[type] = el;
this.resetViews();
},
removeView: function (type) {
delete this.types[type];
this.resetViews();
},
resetViews: function () {
this.$el.children().detach();
_.each(['main_header', 'sub_header', 'sub_sub_header'], function (typekey) {
if(this.types[typekey]) {
this.$el.append(this.types[typekey]);
}
}, this);
}
});
I have been looking at this for quite few hours and I don't think I am able to see the solution.
This is my router.js:
define('router', ['jquery', 'config', 'nav','store'], function ($, config, nav, store) {
var
concepTouch = Sammy('body', function () {
// This says to sammy to use the title plugin
this.use(Sammy.Title);
this.use(Sammy.Mustache);
// Sets the global title prefix
this.setTitle(config.title.prefix);
// So I can access sammy inside private methods
var sammy = this;
function establishRoutes() {
// Defines the main container for content then
var mainConainer = $(config.mainContentContainerId);
// Adds animation loading class to the main container
mainConainer.addClass(config.loadingAnimationCssClass);
// iterates through routes defined in config class then
_.forEach(config.appRoutes, function(obj, key) {
// defines each one as a route
sammy.get(obj.hashV, function(context) {
// Store the requested route as the last viewed route
store.save(config.stateKeys.lastView, context.path);
// Fetches its html template
context.render(obj.tmpltURL, { 'routeData': context.params })
// Appends that htmlo template to the main container and removes loading animation
.then(function(content) {
mainConainer.removeClass(config.loadingAnimationCssClass).html(content);
});
// Finally adds the route title to the prefix
this.title(obj.title);
});
// Overriding sammy's 404
sammy.notFound = function () {
// toast an error about the missing command
toastr.error(sammy.getLocation() + ' Does not exist yet!');
// Go to last visited anf if not
sammy.setLocation(
store.fetch(config.stateKeys.lastView) || config.getDefaultRoute()
);
};
});
}
// Calls for routes to be established
establishRoutes();
}),
// runs concep touch as a sammy App with the initial view of default route
init = function () {
// Try to get today's last visit and if not available then fallback on default
concepTouch.run(store.fetch(config.stateKeys.lastView) || config.getDefaultRoute());
// Make the correct nav item active and add Click handlers for navigation menu
nav.setStartupActiveClass(store.fetch(config.stateKeys.lastView) || sammy.getLocation())
.addActiveClassEventHandlers();
};
return {
init: init,
concepTouch: concepTouch
};
});
This when I submit the search form gets this template for me:
<div id="contacts" class="view animated fadeInLeft">
<h3>Search results for {{routeData}}</h3>
<ul data-bind="template: { name: 'searchresults-template', foreach: searchResults }"></ul>
</div>
<script type="text/html" id="searchresults-template">
<li data-bind="text: type"></li>
</script>
<script>
require(['searchresults'], function (searchresults) {
searchresults.get(to Some how Get routeData.term);
});
</script>
and I can not find the right way to make Mustache pass the data from this line of router.js context.render(obj.tmpltURL, { 'routeData': context.params }) to the {{routeData.term}} inside the template.
{{routeData}} on its own returns `SAMMY.OBJECT: {"TERM": MY SEARCH TERM}`
which I can't navigate to the property i want to from it using . notation. Furthermore even if that worked it can not be passed into Javascript which is what I really need as
searchresults.init(); is waiting for this paramter `searchresults.init(routeData.term);`
Or maybe the answer is to find a way to access sammy's context here? outside of sammy in order to get the params? something like Sammy.Application.context.params['term'] but ofcourse application has no such method so don't know!? :(
Am I going totally the wrong way about it? How Can I easily pass the query string params as accessible objects inside my template so knockout can use it.
Your help is greatly appreciated.
<div id="contacts" class="view animated fadeInLeft">
<h3>Search results for {{#routeData}}{{term}}{{/routeData}}</h3>
<ul data-bind="template: { name: 'searchresults-template', foreach: searchResults }"></ul>
</div>
<script type="text/html" id="searchresults-template">
<li data-bind="text: type"></li>
</script>
<script>
require(['searchresults'], function (searchresults) {
var searchTerm = "{{#routeData}}{{term}}{{/routeData}}";
searchresults.get(searchTerm);
});
</script>
I am building a gallery-like picker view in Backbone.js. The picker has many thumbnail views bound to models within a collection and a large preview view for the currently selected thumbnail. Clicking a thumbnail will fire a Backbone.Event to change the preview view's model. The preview view, however, observes several Backbone events on the model to change state depending on the model's attributes.
I'm having trouble when it comes to unregistering Backbone events on the previous model and re-registering the same events on the new model. I don't always have reference to the original .on() registration, and I am tempted to simply call this.model.off() to unregister all the model's events (I don't want to destroy any other events the model may have, however). The Backbone.js documentation outlines that calling .off(null, null, context) will unregister the events from the object within the current context. I am uncertain, however, if this will unregister all events just for the current view instance.
Let's use this setup to register and unregister events:
var ThumbView=Backbone.View.extend({
initialize: function() {
this.model.on("change:title", this.log, this);
},
log:function(model) {
console.log("Thumb view : "+model.get("id")+" : "+model.get("title"));
}
});
var MainView=Backbone.View.extend({
initialize: function() {
this.model.on("change:title", this.log, this);
},
log:function(model) {
console.log("Main view : "+model.get("id")+" : "+model.get("title"));
}
});
var m1=new Backbone.Model({id:1,title:"m1"});
var t=new ThumbView({model:m1});
var v=new MainView({model:m1});
m1.set({title:"m1, 1"});
v=new MainView({model:m1});
m1.set({title:"m1, 2"});
As is, creating a new MainView won't destroy the previous bindings and will result in a zombie view. The last 3 lines give the following result:
Thumb view : 1 : m1, 1
Main view : 1 : m1, 1
Thumb view : 1 : m1, 2
Main view : 1 : m1, 2
Main view : 1 : m1, 2
The accompanying Fiddle : http://jsfiddle.net/9xufW/
Let's test the off method with a specific context:
var MainView=Backbone.View.extend({
initialize: function() {
this.model.on("change:title", this.log, this);
},
log:function(model) {
console.log("Main view : "+model.get("id")+" : "+model.get("title"));
},
teardown: function() {
this.model.off(null, null, this);
}
});
Calling
m1.set({title:"m1, 1"});
v.teardown(); // "destroys" the old view
v=new MainView({model:m1});
m1.set({title:"m1, 2"});
yields the expected result
Thumb view : 1 : m1, 1
Main view : 1 : m1, 1
Thumb view : 1 : m1, 2
Main view : 1 : m1, 2
http://jsfiddle.net/9xufW/1/
The callbacks set in the thumbviews are preserved while the callbacks set in the main view are removed. Note that the teardown method could and probably should be used to undelegate DOM events.
Another Fiddle where the model in the main view is replaced instead of destroying/recreating the view http://jsfiddle.net/9xufW/2/