$location / switching between html5 and hashbang mode / link rewriting
Asked Answered
P

4

180

I was under the impression that Angular would rewrite URLs that appear in href attributes of anchor tags within tempaltes, such that they would work whether in html5 mode or hashbang mode. The documentation for the location service seems to say that HTML Link Rewriting takes care of the hashbang situation. I would thus expect that when not in HTML5 mode, hashes would be inserted, and in HTML5 mode, they would not.

However, it seems that no rewriting is taking place. The following example does not allow me to just change the mode. All links in the application would need to be rewritten by hand (or derived from a variable at runtime. Am I required to manually rewrite all URLs depending on the mode?

I don't see any client-side url rewriting going on in Angular 1.0.6, 1.1.4 or 1.1.3. It seems that all href values need to be prepended with #/ for hashbang mode and / for html5 mode.

Is there some configuration necessary to cause rewriting? Am I misreading the docs? Doing something else silly?

Here's a small example:

<head>
    <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.1.3/angular.js"></script>
</head>

<body>
    <div ng-view></div>
    <script>
        angular.module('sample', [])
            .config(
        ['$routeProvider', '$locationProvider',
            function ($routeProvider, $locationProvider) {

                //commenting out this line (switching to hashbang mode) breaks the app
                //-- unless # is added to the templates
                $locationProvider.html5Mode(true);

                $routeProvider.when('/', {
                    template: 'this is home. go to <a href="/about"/>about</a>'
                });
                $routeProvider.when('/about', {
                    template: 'this is about. go to <a href="/"/>home</a'
                });
            }
        ])
            .run();
    </script>
</body>

Addendum: in re-reading my question, I see that I used the term "rewriting" without an abundance of clarity as to who and when I wanted to do the rewriting. The question is about how to get Angular to rewrite the URLs when it renders paths and how to get it to interpret paths in the JS code uniformly across the two modes. It is not about how to cause a web server to do HTML5-compatible rewriting of requests.

Perorate answered 21/5, 2013 at 19:0 Comment(1)
Here is the solution for Angular 1.6.Sabinasabine
G
360

The documentation is not very clear about AngularJS routing. It talks about Hashbang and HTML5 mode. In fact, AngularJS routing operates in three modes:

  • Hashbang Mode
  • HTML5 Mode
  • Hashbang in HTML5 Mode

For each mode there is a a respective LocationUrl class (LocationHashbangUrl, LocationUrl and LocationHashbangInHTML5Url).

In order to simulate URL rewriting you must actually set html5mode to true and decorate the $sniffer class as follows:

$provide.decorator('$sniffer', function($delegate) {
  $delegate.history = false;
  return $delegate;
});

I will now explain this in more detail:

Hashbang Mode

Configuration:

$routeProvider
  .when('/path', {
    templateUrl: 'path.html',
});
$locationProvider
  .html5Mode(false)
  .hashPrefix('!');

This is the case when you need to use URLs with hashes in your HTML files such as in

<a href="index.html#!/path">link</a>

In the Browser you must use the following Link: http://www.example.com/base/index.html#!/base/path

As you can see in pure Hashbang mode all links in the HTML files must begin with the base such as "index.html#!".

HTML5 Mode

Configuration:

$routeProvider
  .when('/path', {
    templateUrl: 'path.html',
  });
$locationProvider
  .html5Mode(true);

You should set the base in HTML-file

<html>
  <head>
    <base href="/">
  </head>
</html>

In this mode you can use links without the # in HTML files

<a href="/path">link</a>

Link in Browser:

http://www.example.com/base/path

Hashbang in HTML5 Mode

This mode is activated when we actually use HTML5 mode but in an incompatible browser. We can simulate this mode in a compatible browser by decorating the $sniffer service and setting history to false.

Configuration:

$provide.decorator('$sniffer', function($delegate) {
  $delegate.history = false;
  return $delegate;
});
$routeProvider
  .when('/path', {
    templateUrl: 'path.html',
  });
$locationProvider
  .html5Mode(true)
  .hashPrefix('!');

Set the base in HTML-file:

<html>
  <head>
    <base href="/">
  </head>
</html>

In this case the links can also be written without the hash in the HTML file

<a href="/path">link</a>

Link in Browser:

http://www.example.com/index.html#!/base/path
Garver answered 21/5, 2013 at 19:34 Comment(15)
Thank you for the detailed explanation, @jupiter. I'll look at whether I can skip the bang and keep the hash and trick Angular into not requiring two sets of URLs depending on the mode!Perorate
I think I have not properly understood your problem. Why don't you use just URLs without a hash? They will work in browsers supporting the history API and browsers not supporting the history API. AngularJS will put the # version into the location bar when you click on them in browsers not supporting the history API as AngularJS intercepts clicks on links.Garver
I'm working on a framework that should support both modes. App authors should be able to choose one or the other without worrying about whether or not there are hashes in their templates and/or changes in the interpretation of relative paths. I'm hopeful that your trick will help make it true that "all you do is change the mode" (even if the practical solution is "set the mode to html5 and then lie to angular about the browser capabilities").Perorate
@Garver in Hashbang mode you don't need to use everywhere index.html#!/path. #!/path works for me in Chrome, Firefox, IE and either /#!/ or /index.html#!/ url versions.Lincolnlincolnshire
I'm getting $provide is undefined?Robomb
@user271996 i believe he says index.html#!/path because if you leave it out and goto contact, it will reroute as contact#!/contact rather than index.html#!/contact which at least looks like your never leaving the index page, the other looks confusing.Skedaddle
@Skedaddle @user271996 .hashPrefix('!'); is optional. I happen not to use it.Perorate
@pate -- you need to inject $provide in your config function when you're setting up the decorator.Perorate
Hello, I have implemented Hashbang in HTML5 Mode for my app, the problem is when I try to access directly to url like this: app.localhost/catalog/pvc $routeProvider.when('/catalog/:category', {templateUrl: 'views/catalog.html', controller: 'CatalogController' }); Please, I need some help. the other routes work good but with paremeter does not. Thank youTriglyceride
@Schwertfisch I don't use ngRoute but you might need an absolute URL for your template, i.e. put a slash in front /views/catalog.html. I know that's necessary for ui-router in HTML5 mode because when it seeks the template it's using a base URL of the one in the route (i.e. /catalog in your case).Perorate
@stu.salsbury I solved moving my app to a Symfony view, there was a routing conflict. I appreciate your help. Thank you so much.Triglyceride
Do anchors still work if you add to the end of the URL. For example: http://www.example.com/index.html#!/base/path => http://www.example.com/index.html#!/base/path#anchor?Jaffa
Can we include server configuration for HTML5 Mode in this accepted answer please? github.com/angular-ui/ui-router/wiki/…Honig
For yeoman and latest versions of grunt config file, the configuration that works for me is here: #24284153Proudfoot
@Honig -- configuration of HTML5 mode wasn't part of my question and it was already a pretty complicated question to answer. I think there are a ton of guides out there for configuring HTML5 mode generally. This is a relatively specific issue with angular's on-the-fly rewriting of browser side urls where they appear in your browser code.Perorate
S
8

Fur future readers, if you are using Angular 1.6, you also need to change the hashPrefix:

appModule.config(['$locationProvider', function($locationProvider) {
    $locationProvider.html5Mode(true);
    $locationProvider.hashPrefix('');
}]);

Don't forget to set the base in your HTML <head>:

<head>
    <base href="/">
    ...
</head>

More info about the changelog here.

Sabinasabine answered 22/3, 2017 at 8:45 Comment(2)
thanks @Mistalis. your's answer working fine. but issue when I refresh page after routing, page not found errors occurs. please help me..Ishmaelite
@AshishMehta Hello Ashish. I suggest you to read this answer, it may solve your issue. Bye! :)Sabinasabine
H
0

I wanted to be able to access my application with the HTML5 mode and a fixed token and then switch to the hashbang method (to keep the token so the user can refresh his page).

URL for accessing my app:

http://myapp.com/amazing_url?token=super_token

Then when the user loads the page:

http://myapp.com/amazing_url?token=super_token#/amazing_url

Then when the user navigates:

http://myapp.com/amazing_url?token=super_token#/another_url

With this I keep the token in the URL and keep the state when the user is browsing. I lost a bit of visibility of the URL, but there is no perfect way of doing it.

So don't enable the HTML5 mode and then add this controller:

.config ($stateProvider)->
    $stateProvider.state('home-loading', {
         url: '/',
         controller: 'homeController'
    })
.controller 'homeController', ($state, $location)->
    if window.location.pathname != '/'
        $location.url(window.location.pathname+window.location.search).replace()
    else
        $state.go('home', {}, { location: 'replace' })
Heiskell answered 22/10, 2015 at 8:53 Comment(0)
A
0

This took me a while to figure out so this is how I got it working - Angular WebAPI ASP Routing without the # for SEO

  1. add to Index.html - base href="/">
  2. Add $locationProvider.html5Mode(true); to app.config

  3. I needed a certain controller (which was in the home controller) to be ignored for uploading images so I added that rule to RouteConfig

         routes.MapRoute(
            name: "Default2",
            url: "Home/{*.}",
            defaults: new { controller = "Home", action = "SaveImage" }
        );
    
  4. In Global.asax add the following - making sure to ignore api and image upload paths let them function as normal otherwise reroute everything else.

     private const string ROOT_DOCUMENT = "/Index.html";
    
    protected void Application_BeginRequest(Object sender, EventArgs e)
    {
        var path = Request.Url.AbsolutePath;
        var isApi = path.StartsWith("/api", StringComparison.InvariantCultureIgnoreCase);
        var isImageUpload = path.StartsWith("/home", StringComparison.InvariantCultureIgnoreCase);
    
        if (isApi || isImageUpload)
            return;
    
        string url = Request.Url.LocalPath;
        if (!System.IO.File.Exists(Context.Server.MapPath(url)))
            Context.RewritePath(ROOT_DOCUMENT);
    }
    
  5. Make sure to use $location.url('/XXX') and not window.location ... to redirect

  6. Reference the CSS files with absolute path

and not

<link href="app/content/bootstrapwc.css" rel="stylesheet" />

Final note - doing it this way gave me full control and I did not need to do anything to the web config.

Hope this helps as this took me a while to figure out.

Allophane answered 7/11, 2015 at 15:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.