Backbone.js change url without reloading the page - backbone.js

I have a site that has a user page. On that page, there are several links that let you explore the user's profile. I'd like to make it so that, when one of those links is clicked on, the url changes, but the top third of the page containing the user's banner doesn't reload.
I'm using Backbone.js
I have a feeling that I'm in one of those situation where I have such a poor understanding of the problem I'm dealing with that I'm asking the wrong question, so please let me know if that appears to be the case

My mistake was assuming that there was a special, built-in way of doing this in backbone. There isn't.
Simply running the following line of code
window.history.pushState('object or string', 'Title', '/new-url');
will cause your browser's URL to change without reloading the page. You can open up the javascript console in your browser right now and try it with this page. This article explains how it works in more detail (as noted in this SO post).
Now I've just bound the following event to the document object (I'm running a single page site):
bindEvents: () ->
$(document).on('click', 'a', #pushstateClick)
pushstateClick: (e) ->
href = e.target.href || $(e.target).parents('a')[0].href
if MyApp.isOutsideLink(href) == false
if e.metaKey
#don't do anything if the user is holding down ctrl or cmd;
#let the link open up in a new tab
else
e.preventDefault()
window.history.pushState('', '', href);
Backbone.history.checkUrl()
See this post for more info.
Note that you CAN pass the option pushstate: true to your call to Backbone.history.start(), but this merely makes it so that navigating directly to a certain page (e.g. example.com/exampleuser/followers) will trigger a backbone route rather than simply leading to nowhere.

Routers are your friend in this situation. Basically, create a router that has several different routes. Your routes will call different views. These views will just affect the portions of the page that you define. I'm not sure if this video will help, but it may give you some idea of how routers interact with the page: http://www.youtube.com/watch?v=T4iPnh-qago
Here's a rudimentary example:
myapp.Router = Backbone.Router.extend({
routes: {
'link1': 'dosomething1',
'link2': 'dosomething2',
'link3': 'dosomething3'
},
dosomething1: function() {
new myapp.MyView();
},
dosomething2: function() {
new myapp.MyView2();
},
dosomething3: function() {
new myapp.MyView3();
}
});
Then your url will look like this: www.mydomain.com/#link1.
Also, because <a href=''></a> tags will automatically call a page refresh, make sure you are calling .preventDefault(); on them if you don't want the page to refresh.

Related

calling Backbone.history.navigate(...) leads to recursion?

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...?

Updating URL in Angular JS without re-rendering view

I'm building a dashboard system in AngularJS and I'm running into an issue with setting the url via $location.path
In our dashboard, we have a bunch of widgets. Each shows a larger maximized view when you click on it. We are trying to setup deep linking to allow users to link to a dashboard with a widget maximized.
Currently, we have 2 routes that look like /dashboard/:dashboardId and /dashboard/:dashboardId/:maximizedWidgetId
When a user maximizes a widget, we update the url using $location.path, but this is causing the view to re-render. Since we have all of the data, we don't want to reload the whole view, we just want to update the URL. Is there a way to set the url without causing the view to re-render?
HTML5Mode is set to true.
In fact, a view will be rendered everytime you change a url. Thats how $routeProvider works in Angular but you can pass maximizeWidgetId as a querystring which does not re-render a view.
App.config(function($routeProvider) {
$routeProvider.when('/dashboard/:dashboardId', {reloadOnSearch: false});
});
When you click a widget to maximize:
Maximum This Widget
or
$location.search('maximizeWidgetId', 1);
The URL in addressbar would change to http://app.com/dashboard/1?maximizeWidgetId=1
You can even watch when search changes in the URL (from one widget to another)
$scope.$on('$routeUpdate', function(scope, next, current) {
// Minimize the current widget and maximize the new one
});
You can set the reloadOnSearch property of $routeProvider to false.
Possible duplicate question : Can you change a path without reloading the controller in AngularJS?
Regards
For those who need change full path() without controllers reload
Here is plugin: https://github.com/anglibs/angular-location-update
Usage:
$location.update_path('/notes/1');
I realize this is an old question, but since it took me a good day and a half to find the answer, so here goes.
You do not need to convert your path into query strings if you use angular-ui-router.
Currently, due to what may be considered as a bug, setting reloadOnSearch: false on a state will result in being able to change the route without reloading the view. The GitHub user lmessinger was even kind enough to provide a demo of it. You can find the link from his comment linked above.
Basically all you need to do is:
Use ui-router instead of ngRoute
In your states, declare the ones you wish with reloadOnSearch: false
In my app, I have an category listing view, from which you can get to another category using a state like this:
$stateProvider.state('articles.list', {
url: '{categorySlug}',
templateUrl: 'partials/article-list.html',
controller: 'ArticleListCtrl',
reloadOnSearch: false
});
That's it. Hope this helps!
We're using Angular UI Router instead of built-in routes for a similar scenario. It doesn't seem to re-instantiate the controller and re-render the entire view.
How I've implemented it:
(my solution mostly for cases when you need to change whole route, not sub-parts)
I have page with menu (menuPage) and data should not be cleaned on navigation (there is a lot of inputs on each page and user will be very very unhappy if data will disappear accidentally).
turn off $routeProvider
in mainPage controller add two divs with custom directive attribute - each directive contains only 'templateUrl' and 'scope: true'
<div ng-show="tab=='tab_name'" data-tab_name-page></div>
mainPage controller contains lines to simulate routing:
if (!$scope.tab && $location.path()) {
$scope.tab = $location.path().substr(1);
}
$scope.setTab = function(tab) {
$scope.tab = tab;
$location.path('/'+tab);
};
That's all. Little bit ugly to have separate directive for each page, but usage of dynamic templateUrl (as function) in directive provokes re-rendering of page (and loosing data of inputs).
If I understood your question right, you want to,
Maximize the widget when the user is on /dashboard/:dashboardId and he maximizes the widget.
You want the user to have the ability to come back to /dashboard/:dashboardId/:maximizedWidgetId and still see the widget maximized.
You can configure only the first route in the routerConfig and use RouteParams to identify if the maximized widget is passed in the params in the controller of this configured route and maximize the one passed as the param. If the user is maximizing it the first time, share the url to this maximized view with the maximizedWidgetId on the UI.
As long as you use $location(which is just a wrapper over native location object) to update the path it will refresh the view.
I have an idea to use
window.history.replaceState('Object', 'Title', '/new-url');
If you do this and a digest cycle happens it will completely mangle things up. However if you set it back to the correct url that angular expects it's ok. So in theory you could store the correct url that angular expects and reset it just before you know a digest fires.
I've not tested this though.
Below code will let you change url without redirection such as: http://localhost/#/691?foo?bar?blabla
for(var i=0;i<=1000;i++) $routeProvider.when('/'+i, {templateUrl: "tabPages/"+i+".html",reloadOnSearch: false});
But when you change to http://localhost/#/692, you will be redirected.

Change route parameters without updating view

I'm currently trying to figure out how to change the route parameters without reloading the entire page. For example, if I start at
http://www.example.com/#/page
but update a name to be 'George', to change the route to be:
http://www.example.com/#/page/george
If I already had http://www.example.com/#/page/:name routed.
Without reloading the location. Can one just set $routeParams.name = "George" ?
Edit:
Alternatively, is there a way to update http://www.example.com/#/page?name=George without reloading or resetting the page?
Ok, after a lot of searching. I answered my own question.
I've discovered finding anything on the angular documentation is incredibly impossible, but sometimes, once it's found, it changes how you were thinking about your problem.
I began here: http://docs.angularjs.org/api/ng.$location
Which took me here: http://docs.angularjs.org/guide/dev_guide.services.$location
Which took me to this question: AngularJS Paging with $location.path but no ngView reload
What I ended up doing:
I added $location.search({name: 'George'}); To where I wanted to change the name (A $scope.$watch).
However, this will still reload the page, unless you do what is in that bottom StackOverflow link and add a parameter to the object you pass into $routeProvider.when. In my case, it looked like: $routeProvider.when('/page', {controller: 'MyCtrl', templateUrl:'path/to/template', reloadOnSearch:false}).
I hope this saves someone else a headache.
I actually found a solution that I find a little more elegant for my application.
The $locationChangeSuccess event is a bit of a brute force approach, but I found that checking the path allows us to avoid page reloads when the route path template is unchanged, but reloads the page when switching to a different route template:
var lastRoute = $route.current;
$scope.$on('$locationChangeSuccess', function (event) {
if (lastRoute.$$route.originalPath === $route.current.$$route.originalPath) {
$route.current = lastRoute;
}
});
Adding that code to a particular controller makes the reloading more intelligent.
You can change the display of the page using ng-show and ng-hide, these transitions won't reload the page. But I think the problem you're trying to solve is you want to be able to bookmark the page, be able to press refresh and get the page you want.
I'd suggest implementing angular ui-router Which is great for switching between states without reloading the page. The only downfall is you have to change all your routes.
Check it out here theres a great demo.

Whats the Advantage of Marionette AppRouter+Controller over Backbone.Router?

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();
}

Backbone Navigates to Base URL Immediately After Page Load

I've been facing this issue with Backbone routing and figured I'd spent enough time investigating:
There are two urls at play here: / and /post/:id. The / page has links to various posts via /post/:id. When I click the post link, the post page loads, but backbone immediately changes the url to /. Not only does this look bad, it also triggers route handlers at the wrong time. I'm not doing anything special... here's my code:
PostRouter = Backbone.Router.extend({
routes : {
"" : "doHome"
},
initialize : function() {
},
doHome : function() {
// do some stuff before navigating
window.location = "/";
}
})
...
var router = new PostRouter();
Backbone.history.start({ pushState: Modernizr.history });
Again, the doHome function is called immediately after the post page loads. Clearly this causes the site to navigate back to the home page. I can obviously remove that call to window.location to prevent that, but the url still gets updated to the root url, which isn't acceptable.
Thanks in advance!
UPDATE 1:
If I go directly to "localhost:808/post/:id" the url immediately changes to "localhost:8080/". However, if I do this exact same thing in private browser window, this behavior is not observed.
UPDATE 2:
Given what I found in update 1, I went crazy and started from scratch: I cleared 4 weeks of browsing history (sigh), stopped my local server and cleaned up all persistent sessions and redeployed my app. Alas, it worked! That said, I am not listing this as a solution as it doesn't help explain what exactly is going on and how to solve it. Additionally, it leaves me concerned about this happening to users of my site. I'd have no way to tell that this was happening and, even if I did, I couldn't tell them how to fix it on their end (clearing 4 weeks of browser history is not an option!). Can anyone shed some light on what might have been going on?
why don't you try to add
console.log(Backbone.history.handlers);
at the end to see, how your rout is added to Backbone.history. This might shed some light.

Resources