Marionette PushState URL Handeling - How to do it? - backbone.js

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.

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 :)

How to deal with extra hash in route? (AngularJS 1.5 + new/component router)

We're attempting to build an app using Angular 1.5 with the new component router bits. We've run into a bit of an edge case and we're wondering if there's any way around it.
The Key Players
IdentityServer v2: our client uses this for OAuth currently. It causes a part of this problem. It's legacy, and we don't have control over its usage.
AngularJS 1.5 as our front-end framework.
The new angular router, called ngComponentRouter now I believe? We figured this style would help us bridge between Angular v1.5 and Angular v2, and it was easy enough to port.
oauth-ng as a wrapper for our OAuth implicit flow..
Older browsers: In the sense that we have to support IE9+, meaning we can't use Angular's HTML5 mode.
The Goal
We'd like to take a URL such as http://mysite/#!/auth/#auth_token=xyz123 (structure not under our control, e.g. can't remove second hash) and:
Get it into an actual auth controller
Have the auth_token value available, either through parameters or directly through $location. (it currently is scrubbed before it ever gets to the controller).
Background / Problem
Our client has a central login system where they're using IdentityServer v2. As far as I understand, when we request a token from IdSrv v2, it responds by appending #auth_token=xyz123 to your redirect URL. It was written back when it thought you'd have my.com/login.html, thus resulting in login.html#auth_token=xyz123.
With an Angular app that uses a hash already, though, it becomes a problem, as the URL ends up along the lines of mysite.com/#/auth#auth_token=xyz123.
This, as you might expect, makes Angular angry. We have yet be able to find a way to get this to work under the component router.
How it Would Work With the Older Routers
Per the oauth-ng docs, if we were using the older router without html5 enabled, we'd do something like the following:
angular.module('app').config(function ($routeProvider) {
$routeProvider
.when('/access_token=:accessToken', {
template: '',
controller: function ($location, AccessToken) {
var hash = $location.path().substr(1);
AccessToken.setTokenFromString(hash);
$location.path('/');
$location.replace();
}
})
What We've Tried
Defining a component route in a similar way. This didn't work, because /access_token=:accessToken contains an =, which doesn't appear to be allowed by component router.
Seeing if we can get IdentityServer v2 to change the format of the response. It doesn't seem like it's possible; the response seems to be hard-coded to [URL we define]#auth_token=xyz123.
Faking out the URL using other hashes, etc. Generally wound up with bad / inconsistent behavior.
What We Think our Options Are
Use a catch-all / not found controller. If we let the route fall all the way through to /**, we can retrieve the token value from $location. That's sort of gross though; we'd like to avoid it.
Find a way to get the full URL into the controller. We can capture the route and put it through to a controller, but the URL isn't available at that point.
Go back to using the older router or ui-router (which we'd like not to do at this point).
Anything that could point us in the right direction would be greatly appreciated!
To follow up on this: in the end, we decided that this was a strange enough edge-case that it warranted returning to ui-router.
While we think the component router makes the most sense, the deciding factor here is that, unfortunately, we don't have 100% control over our routes. The route constraint included the edge case that component-router did not seem capable of handling at the current time.
For those who are working with older oauth server systems, I hope this will serve as a warning / some background as you're making your choice of router.
Hopefully ngComponentRouter will better support this edge case in the future, though I wouldn't blame them for leaving it out.

Using UIRouter templates with MVC 4 Routing

I'm trying to create an ASP MVC 4 project with Ui-Router, however I've come across a problem.
My current server-side routing configuration looks like this:
// Controller/Action route
routes.MapRoute(
name: "Default",
url: "{controller}/{action}");
// Redirect any other routes to Site/Index so AngularJS can handle routing
// Place routes above this otherwise they will be ignored
routes.MapRoute(
name: "Catch-All Redirect to Index",
url: "{*url}",
defaults: new { controller = "Site", action = "Index" }
);
And client-side
angular.module('loluk.home')
.config(['$stateProvider', function ($stateProvider) {
$stateProvider.state('index', {
url: '',
templateUrl: '/home/index'
});
}]);
Site/Index, the redirect action, simply contains the HTML file that has the ng-app directive. Any other actions/controllers will return API data, or a template. So, in my case, I have Home/Index which returns a template containing <h1>Hello, World!</h1>.
This is all fine and dandy if one of my states in ui-router requests "home/index" via templateUrl for my application. However, the problem I have now is that if I browse to http://localhost/Home/Index, I will see the template in it's raw form - rather than what I am expecting to see, which is the whole application in the Home/Index state. This makes sense as that is how I have configured it.
I initially thought "OK, well I can solve this problem by redirecting everyone to Site/Index (where the main file is stored) and using inline templates". Well, this works well, until you consider that
The HTML file containing index.html is going to get ridiculously large and contain every template
This breaks escaped_fragment crawling
So right now I am at a loss of how to make this work; I could use inlining, but that would make web pages load slowly and break SEO. I could stick with what I have.. but that will break any bookmarks that end-users create.
Making template calls a ChildActionOnly worked well until the fact that ChildActionOnly will return a server 500 (rather than a redirect), and UI-Router appears to not qualify as a "Child Action" as requesting the template through templateUrl also triggered the server 500.
I did come across this question, however it doesn't express how exactly to solve the template situation.
Another avenue I have just pursued is having a templates area that contains all of my templates, and an api area that contains all of my api details (/templates/ and /api/ respectively). This solves the whole reloading page problem, though I am still unsure of how to approach the escaped_fragment crawling from this point of view.
I've accomplished this by creating two Areas in MVC - one for API that routes to /api/ and one for Templates that routes to /templates/. AngularJS will make calls to /template/{controller}/{action} which will return a plain HTML view, and make RESTful calls to /api/{controller} for retrieving data.
It's not a perfect solution but it works.

Backbone Routes Not Being Called

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>

Backbone.js making router seo friendly

I am new to backbone.js so this may be an easy question.
The router seems to use semantically sensible urls, but they aren't very user friendly.
In the examples I've seen they use:
local.com/post/id/1
But in wordpress I would opt to rewrite this as:
local.com/2012-11-03-backbone-js-router-question.html
This would be run through a big rewrite table and translated to the right controller/action. I don't want to expose the router to the user that prominently.
My final application for this question is with e-commerce in mind:
cms page local.com/about-us.html
product page local.com/blue-tooth-headset.html
category page local.com/phones.html
product via category page local.com/phones/blue-tooth-headset.html
So my question is, how would one get pretty urls while using backbone.js?
My thoughts on options are:
You don't get seo friendly urls
You have some enormous map, and it has to sit on the client :(
Everytime you are about to change the url you have a quick ajax lookup up for the pretty-url, and you need to wait for the pretty-url to come back before you pushState.
sections are prefixed with a letter ie local.com/p/product-name.html and p actually serves to distinguis which router, another could be c for category
Backbone routes can be mostly anything. About e-commerce, you'd just need to use wordy urls:
<category>/<product-name>
television/samsung-i178
But, that's not really what you should matter about regarding SEO. Google mostly added way less value in URLs keyword after last year "panda" update.
What you should really consider is getting fallback and content served on the page even if javascript isn't enabled. But the trouble there is that you probably don't want to code your site twice: frontend templating/paging engine + plus same thing on the backend.
There's upcoming technologies who'll help leverage this problem using node.js (Mojito, Meteor, etc), but right now they're not the most stable projects out there. And, I think it may be a little early to use these in production; if you don't have a really competent team to get you server going and debug those projects if needed.
Anyway, what I mean is that if SEO weight a lot in your project, just don't use backbone.
Edit :
About what you add to your question, I think that's pretty easy to conceptualize.
In Backbone, you use a variable router, like so:
routes: {
"product/:url" : "product"
}
product: function( prettyUrl ) {
console.log( prettyUrl );
// Then you fetch your server
server.fetch( prettyUrl );
}
Then on your server database, you just fetch your database by the product pretty url, like SELECT * FROM product WHERE prettyurl=$prettyURL (Or something similar to this, been a long time since I used mySQL).
This way, you don't have to keep a map on the client side, you only use the pretty url the server gave you to fetch full product.
So when on your collection you call fetch, the server should return to you:
{
id: 1,
name: "Product XYZ",
prettyUrl: "product-xyz"
},
{
id: 2,
name: "Mac Book Pro",
prettyUrl: "mac-book-pro"
}
This way, every pretty url is managed with his model, not in the router. And that's definitely the way to go to manage such URLs. And that's mostly how does it any Wordpress or Drupal out there. Only they do it on the backend side.
There is an HTML5-only solution which allows you to keep urls pretty within Backbone.
Initialize Backbone.history with pushState set.
http://documentcloud.github.com/backbone/#History-start
Beware:
If your application is not being served from the root url / of your domain, be sure to tell
History where the root really is, as an option:
Backbone.history.start({pushState: true, root: "/public/search/"})
Then call history.navigate like this:
navigateToTodo () {
Backbone.history.navigate('/todos/' + this.model.toJSON().id, {trigger: true});
return this;
},
Or just use conventional href linking throughout your app.
Example and explanation here:
From Hashbangs to HTML5 PushState
Fallback for IE:
Fallback for IE
EDIT: So after a while and getting my head around similar problems now I think I understand your question. What you want is not related to Backbone it is to converting a text like this:
"Marissa Mayer can't stop acquiring things" to this "marissa-mayer-can-t-stop-acquiring..."
**Your question is missleading as you supposed Backbone was doing this for you, which it does not.
So the answer is:
What you need is Urlify (there are others, but not quite as good in my experience).
https://github.com/aliem/urlify
-I'll leave the old answer as it seems to have helped people who searched for this-

Resources