Backbone Routes Not Being Called - backbone.js

I have a strange issue I haven't been able to figure out as of yet. It's very simple which is probably why I'm having trouble with it :)
First, here's the routing table...
routes: {
'': 'root', //called
'report': 'report', // called
'report/add': 'reportAdd', // not called
'report/print': 'reportPrint', // not called
'report/settings': 'reportSettings', // not called
},
You'll see I marked which ones are working and which ones aren't. The problem boils down to all subroutes (i.e report/add) not being matched.
Backbone history is called properly in main.js like so:
app.Router = new Router();
Backbone.history.start({ pushState: true });
Obviously, that's in the right spot because routes are working just not sub-routes. I've tried the root options of Backbone.history and the silent parameter all without any luck.
I imagine it's a configuration/setup issue but I haven't been able to find any answers. What am I doing wrong? Any help is much appreciated.
Btw, I'm using requirejs and Backbone Boilerplate but I don't see how that would make a difference.
UPDATE: Although the answer provided is technically correct, the problem is with Backbone Boilerplate. See the bottom of this blog post for an explanation. I'm having the same issue as the first commenter there.

As discussed in the comments, the problem is that, when using push-state style URLs, the server doesn't recognize the Backbone route URLs.
For illustration, say your application's root is at server/app/index.html, and you're trying to use a URL that Backbone routes to /report/print. With URL fragment routing, this is fine:
http://server/app/index.html#report/print
The server ignores the part after # and returns index.html; then on load Backbone routes to report/print.
But if you're using push-state routing, then the URL looks like this:
http://server/app/index.html/report/print
And the server throws a 404 error because it doesn't recognize anything at that path, so Backbone is never even loaded.
The solution is to either:
As the Backbone.js docs note, modify server code, so that the server renders the correct content for each Backbone route, or
(which I think is easier) put a URL rewrite in place on the web server (IIS, Apache), so that it will return index.html for any request that is a Backbone route like index.html/report/print, index.html/report/add, etc.
In IIS, for example, you'd put the following in the web.config under your application root:
<rewriteMaps>
<rewriteMap name="StaticRewrites">
<add key="index.html/report/print" value="index.html" />
<add key="index.html/report/add" value="index.html" />
<!-- etc -->
</rewriteMap>
</rewriteMaps>

Related

AngularJS and NodeJS http-server: rewrite URL

I'm using NodeJS http-server to code an AngularJS App.
I'm having a problem. When I try to access directly the url in the browser the Angular does not intercept the URL to show me the content.
If I type the URL manually like: http://127.0.0.1:8080/#!/clients it works, but not when I type directly: http://127.0.0.1:8080/clients
I want the http://127.0.0.1:8080/#! as default in the http-server.
I'm using in AngularJS the html5 mode and the hash prefix:
$locationProvider.html5Mode(true);
$locationProvider.hashPrefix('!');
Is there a way to rewrite the url from http-server with the default /#!/ before the address?
Note: Below is an example of a more complex Express.js URL rewriting situation, where you may not wish to "catch all" routes, but instead discern between routes for views and routes for the server-side api. All solutions I could find only showed a generic catch-all approach, which did not fit practical application for an app whom requires access to server-side routes. If you do simply want the "catch all", see the other yellow note at the bottom of this answer for links on how to set that up.
If you turn off html5 mode, by default, you should be in hashbang (#!) mode...Turning on html5Mode allows you to remove the hashbang (#!), if you wish.
Here's more about the different modes:
https://docs.angularjs.org/guide/$location
HTML5 mode being enabled, gives you normal looking, non-hashbang URLs. Normally, when HTML5 mode is enabled, you'll hit the issue you described where pages within your app will load OK internally, but if you try and enter the URL directly into your browser (or access it via a bookmark), you'll get a Cannot GET error.
First, be sure you've set a <base> tag in the <head> your primary index file, like this:
<head>
<!-- all your script tags, etc etc -->
<base href="/">
<!-- rest of your front-end dependencies etc -->
</head>
That way Angular will know which is your primary index to load partials within.
Secondly, I will try and tell you how I approached re-writing my URLs in Express to solve the issue you have described. My answer may be somewhat incomplete, as I am still learning, and in truth, I do not fully understand why, once HTML5 mode is enabled in Angular, that the routing does not work properly. There also may be better ways to approach the problem as opposed to how I solved mine.
It seemed that once I switched to HTML5 mode, Angular intercepted my routes and was causing an issue when I was trying to use the $http service to request server-side api routes.
It seemed like turning on HTML5 mode basically took over all of my routing, and I had to find a way to tell Express to either pass a route to Angular or to continue the route (away from angular) using next(). This was my best assessment.
What I Did:
Enabled HTML5 mode [as you have done in your example], and set a
<base> in my index file (as noted above).
Rewrote my routing in ExpressJS using the native express.Router():
See: https://expressjs.com/en/guide/routing.html
At the very bottom of that page are instructions for express.Router()
I'll show you how I did it below
My Approach/Pseudo-code:
I setup a method in my Router so that if the incoming request contained /api/ (checked via regex), I would invoke ExpressJS's next() method and continue along in the route, which would hit the server controller. Otherwise, if the URL did not contain /api/, I the appropriate view page was delivered, in which Angular took over.
How I setup my express.Router():
I created a middleware folder in my app and created a file called api-check.js.
// Setup any Dependencies:
var express = require('express'),
router = express.Router(), // very important!
path = require('path');
// Setup Router Middleware:
/*
Notes: The `apiChecker` function below will run any time a request is made.
The API Checker will do a regex comparison to see if the request URL contained
the `/api/` pattern, to which instead of serving the HTML file for HTML5 mode,
it will instead `next()` along so the API route can reach the appropriate
server-side controller.
*/
router.use(function apiChecker (req, res, next) {
console.log('/// ROUTER RUNNING ///'); // will print on every route
console.log('URL:', req.originalUrl); // will show requested URL
var regex = /(\/api\/)/g; // pattern which checks for `/api/` in the URL
if (regex.test(req.originalUrl)) { // if the URL contains the pattern, then `next()`
console.log('THIS IS AN API REQUEST'); // api detected
next();
} else { // if the URL does not contain `/api`:
res.sendFile(path.join(__dirname, './../../client/index.html')); // delivers index.html which angular-route will then load appropriate partial
}
})
module.exports = router; // exports router (which now has my apiChecked method attached to it)
How I added my express.Router() to my Express App:
Depending upon how your code is modularized, etc, just go ahead and in the right place require your module (you will have to adjust the direct path depending upon your project), and then app.use() the module to intercept all of your routes and direct them to your Router:
// Get Router:
var apiCheck = require('./../middleware/api-check');
// Use Router to intercept all routes:
app.use('/*', apiCheck);
Now, any route (thus the /*) will go through the express.Router() apiChecker() function, and be assessed. If the requesting URL contains /api, then next() will be invoked and the server-side controller will be reached. Otherwise, if the /api slug is not detected in the URL, then the primary index.html base file will be sent, so that Angular can deliver the appropriate view via $routeProvider.
Note: If you don't need to discern between incoming routes, and just want to "catch all" incoming routes and hand back your <base> index file, you can do as outlined in another stackoverflow answer here. That answer uses app.get() to catch all GET requests to hand back your index. If you also need to catch POST requests or others, you may want to instead use app.all(), in place of app.get() in the aforementioned example. This will catch all routes, whether GET, POST, etc. Read more in the Express documentation here.
This was my personal solution, and there may be better, but this solved my problem! Would be interested to know what others recommend! Of course the downside to this, is that I have to build all of my internal api routes to include /api/ in them, however that seems to be OK in design overall, and maybe even useful in keeping me from confusing my routes from front-side views.
Hope this at least helps somewhat, let me know if you need any clarifications :)

Passport.js, Express.js, and Angular.js routing: how can they coexist?

I apologize this question turned out a bit long, but I have worked on this for some time and really needed to explain all the story.
Background: App based on MEAN stack, trying to authorize Facebook logins using Passport.js.
Following Passport.js guide I implemented something similar to:
// HTML
Add a Facebook login
// send to facebook to do the authentication
app.get('/connect/facebook',isLoggedIn, passport.authorize('facebook',
{ scope : 'email' })
);
// handle the callback after facebook has authorized the user
app.get('/connect/facebook/callback',
passport.authorize('facebook', {
successRedirect : '/profile',
failureRedirect : '/profile'
}));
Notice the target=_self in the html in order to skip Angular routing.
Clearly, authorization works fine. However, redirection does not work, as the routing is handled by Angular. After authorization I never land on /profile (but on the default Angular route).
Therefore, I tried with a custom callback as suggested by Passport.js here, with the hope of passing json data to Angular, and let Angular do the routing. I ended up doing something like:
// In the controller
$http.get("/connect/facebook").success(function(data){
// here I wait for json data from the server and do the routing
});
// I call this route from Angular
app.get('/connect/facebook',isLoggedIn,passport.authorize('facebook',
{ scope : 'email' })
);
// But Facebook lands here!
app.get('/connect/facebook/callback',function(req, res, next) {
passport.authorize('facebook', function(err, user, info) {
res.json({something:smtg});
...
Clearly custom callbacks work for local-login, as Passport.js explains. But here do you see the problem? I call /connect/facebook from Angular, but I should receive some json from /connect/facebook/callback.
I am about to give up Passport, but before this, do you see any solution which would allow landing on /profile after FB authorization, perhaps with a custom message? Many thanks for reading through.
EDIT:
The same question had been reported as an issue on the Passport-Facebook GitHub account. Some additional attempts have been posted there, but not quite the fix yet.
This is a bit more in depth than can be described in one answer, but I'll try to start pointing you in the right direction.
Essentially, Angular.js routes are not really HTML routes at all, but an internal route structure that happens to use the URL for use of the end user. Remember that Angular.js is a client script, and that a full page reload is not desired, as this will reload the entire script. Therefore, /# is used to trick the browser into jumping to a specific bit of code within the already loaded script. (as opposed to the traditional anchor location in the HTML document). Unfortunately (or fortunately), HTML 5 mode allows you to hide the /# part of the url, so instead of seeing http://somesite.com/#/someroute you just see http://somesite.com/someroute. Rest assured, however, that the /# is still there. Angular.js uses the HTML5 pushState (AKA HistoryAPI) to perform the magic replacement.
Given this, if you have called a server route, you are outside the Angular.js script, and any call to load the angular script again will start from the very beginning. You can't actually call your Angular.js route from the server without a full reload. Therefore, you are really doing a double route redirect here. Your server should be calling it's default route for angular, appending /#/someroute to the call. The angular.js page will load, parse off the /#, and redirect to the correct angular route. Keep in mind, however, that if there was any dependency on already loaded objects, those are no longer in memory. Therefore, any route accessed this way should operate as if it is an entry point to your application.
Effectively, you should try using successRedirect : '#/profile', keeping in mind that the profile route in angular should be treated as an app entry point.
Hopefully this gets you started.
If #Claies's way is not working, is it possible you have not get rid of the #= fragment from the facebook callback.
Have a read of this post

Backbone router: Use hashbangs

I was setting up a simple router in Backbone.js while I noted an issue, I googled for a long time and found some solutions.
The problem is that whenever I navigate to a url, this url will work while I'm inside the app itself, but once you bookmark it and return it will obviously return a 404 since that page doesn't exist.
Backbone.history.navigate('test', true);
This will navigate to http://www.domain.com/test, which is great, but once you refresh the same URL it returns a 404.
There are ways to get this to work with using a specific .htaccess file, but I'm building this inside an existing website, so I can't use this.
Is there a way in Backbone.js to revert to just using hashbangs? My url would then look like this `http://www.domain.com/#!/test
This might not be the cleanest URL, but at least it will work.
Or am I just missing something obvious here? It can't be that I'm the only person with this problem.
You're using pushState according to your url, so you've got 2 main options:
stop using pushState when starting Backbone history (which would be the equivalent of using a hashbang, except for SEO)
have your server return a page for every URL your app generates (it can be the same exact page)
In the second case, if you don't care about SEO, you can have a catch all rule always returning (e.g.) index.html containing your Backbone app. When you start Backbone's history, it will lokk at the URL and navigate to the proper page (although you might have a "double render" effect).
If you're worried about SEO, you need to return page content for SEO along with your Backbone app.

Marionette PushState URL Handeling - How to do it?

I have been unable to find any articles on Backbone.Marionette using pushstate withOUT Node.js, or grunt, or require where a serious discussion is made about URL handeling. A user should be able to send a link to an internal page to her grandmother and have the link work, for example. The pushstate functionality seems uniquely ill-suited to the real world. Can someone comment and perhaps provide some links to serious articles on the subject?
Generally speaking, there's really just 2 things you need to do...
Tell Backbone to use pushstate by calling Backbone.history.start({ pushState: true });. You may also need to add something like root: 'myApp' to that parameter if your Marionette app is served from http://example.com/myApp instead of directly on http://example.com.
Configure your web server so that no matter what URL the user requests, the same content (your Marionette app) is returned (without doing a redirect).
The catch is that #2 is implemented differently depending on what type of web server you are using. And you haven't told us what stack you're on. In ASP.NET, for example, this can be handled by setting up a 'catch all' route by adding something like this to your RouteConfig.cs file:
// all requests (except those explicitly handled by another route)
// go to HomeController.Index and then the Backbone router examines
// the URL client-side to determine client-side what to do
routes.MapRoute(
name: "Default",
url: "{*clientRoute}", // this is wildcard which captures the entire URL
defaults: new { controller = "Home", action = "Index" }
);
Other web server stacks will have different ways of achieving the same type of thing.

Bookmarking and page reloads with Backbone.js and pushState

I've been trying to get various routes bookmarkable within my app, and this is possible if I don't have pushState enabled. A user can enter mysite.com/#/view/30 and the proper view renders.
However, if I were to enable pushState and go to mysite.com/view/30 I receive a parse error (navigating there via the root page works fine).
I'm currently utilizing the Backbone.js Boilerplate using Require.js, and the parse error is appearing in my config.js file. I'm fairly certain the issue isn't with Require but I'm not completely sure. Frankly, I don't necessarily know what code to paste here either because I believe I'm more lacking a fundamental understanding a difference between hashbangs vs. pushState.
I've read up on the HTML5 feature, however the answer to my specific question still eludes me.
The page with your Backbone router on does not exist at the path you are pointing to in your pushState. The pushState is purely to change the URL representation. You'd have to do some server config changes to route all of your requests back to a main index file.

Resources