Fun with URLs in an Angular App

Share on facebook
Share on twitter
Share on linkedin
Share on email

This blog post is going to help you clean up your URLs when you’re developing a SPA (Single Page App). In addition, I’m going to show you how to enable a browser refresh when you do. And finally, I’m going to show you how to enable ASP.NET MVC’s ReturnUrl functionality when the target page is an Angular route. If any of these things interest you, then keep reading.

What do I mean about cleaning up URLs? Well, if you’ve ever clicked around an Angular web site, you might have noticed the “#” (hashtag, or pound) in the URL, like this:

http://myurl.com/#/customer/list

This is not specific to Angular, it applies to all SPAs. In a nutshell, the path before the pound will be handled by your server router, the path after the pound will be handled by your Angular router. The default page in your web app will be responsible for initializing and launch your Angular bits, and will decide which view to load. In our web applications, we typically have one MVC controller (HomeController) which loads a razor Index.cshtml page. This page bootstraps Angular and from that point on, we’re dealing with Angular views.

So what’s the problem with the pound? Well, it’s kinda ugly, isn’t it? I think the URL would look much better without it. Wouldn’t we all look a little better if we dropped a few pounds? But more importantly, the presence of the pound indicates to all your users that your web site is a SPA. Oh sure, they’ll be able to tell from the snappy response times and glorious user experience; but I’ve never liked giving away the underlying technology in my URLs (especially when the extension is cfm, but that’s a subject for another post).

It is in fact very simple to configure your application so that the pound is not used. However, when you do, there are a few gotchas. This blog post will describe how to get rid of the pound from the URL and how to resolve the issues that arise.

The technologies being used are Angular 1, ASP.NET Web API 2, ASP.NET MVC 6 and IIS 8. This post assumes you have a working knowledge of each.

Dropping the Pounds

As I indicated earlier, getting rid of the pound is very easy. In your Angular config function (probably in app.js), inject $locationProvider and include the following line:

$locationProvider.html5Mode(true);

And that’s all there is to it! Save this change, refresh your page, and the pound will magically disappear.

Handling Navigation between Pages

If you didn’t plan for this ahead of time and you’ve already been creating pages, there’s a good chance you’ve just broken your page navigation. Every page link will have to be modified to omit the pound. So you’ll have to change these:

<a href=”/#/customer/list”>Customer List</a>

To this:

<a href=”/customer/list”>Customer List</a>

And you’ll have to change your controller navigation from these:

$location.url(“/#/customer/list”);

To this:

$location.url(“/customer/list”);

When I first started out with Angular, I needed the pounds during development. I was changing pages and needing to refresh the browser to see those changes. Unfortunately, without some tweaking, browser refreshes do not work (more on that in the next section). I wanted a way to be able to toggle in and out of pound mode without breaking all my page links, so my URLs would look pretty in production, without the overhead of having to change all my page navigation. Surely there’s an easier way. Lucky for us, there is. And it’s called UI-Router.

UI-Router (https://github.com/angular-ui/ui-router/wiki) saves the day. It’s a wonderful and very popular tool. It’s loaded with functionality, but I’m most interested in its feature where you name your routes.

Instead of using the out-of-the-box Angular router, you use UI-Router’s $stateProvider. Setting up a route looks like this:

        $stateProvider
            .state('customerList', {
                url: '/customer/list',
                templateUrl: '/app/components/customer/list.html',
                controller: 'customers'
            });

The syntax is very similar to the regular router, but you’ll notice we passed “customerList” in to name the state for the customer list page. Now, when you create a page link, you can do this:

<a ui-sref=”customerList”>Customer List</a>

“ui-sref” is a UI-Router directive, and it replaces the standard href. If you’re navigating to a page from Angular, you can do this:

$stage.go(“customerList”);

In an app that uses UI-Router, your html and JavaScript are completely absent of URL paths. This enables you to safely toggle in and out of pound mode and UI-Router will render the correct links for you.

The Browser Refresh

The reason I wanted pounds in the URL to begin with is so that I could refresh a page in the browser to load my latest changes. This is usually a problem with a SPA because when you refresh a page, it gets a new request for the URL and the server has no way to know that the path corresponds to an Angular route. So in this case, MVC pukes and gives us a page not found.

I found a number of different ways to solve this issue, but the one I found most effective was to include a rewrite section in your web.config, like this:

    <rewrite>
      <rules>
        <rule name="Main Rule" stopProcessing="true">
          <match url=".*" />
          <conditions logicalGrouping="MatchAll">
            <add input="{REQUEST_URI}" pattern="api/" ignoreCase="true" negate="true" />
            <add input="{REQUEST_URI}" pattern="Home/" ignoreCase="true" negate="true" />
            <add input="{REQUEST_URI}" pattern="Account/" ignoreCase="true" negate="true" />
            <add input="{REQUEST_URI}" pattern="Manage/" ignoreCase="true" negate="true" />
            <add input="{REQUEST_URI}" pattern="bundles/" ignoreCase="true" negate="true" />
            <add input="{REQUEST_URI}" pattern="Content/" ignoreCase="true" negate="true" />

            <!-- Static files and directories can be served so partials etc can be loaded -->
            <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
            <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
          </conditions>
          <action type="Rewrite" url="/" />
        </rule>
      </rules>
    </rewrite>  

The entries will vary depending on your application, but the general idea is you include all the paths that correspond to MVC controllers or physical files or directories. In an ideal world, we wouldn’t be using MVC controllers at all, but the MVC Visual Studio project templates provides some great out of the box functionality, such as dealing with user management and authentication. Just be careful that you don’t have an MVC route that is the same as an Angular route, otherwise confusion will ensue. In order for this to work, you have to install the IIS Rewrite Module, and it isn’t installed by default. It can be downloaded from here: http://www.iis.net/downloads/microsoft/url-rewrite.

Deep Links

If you want a user to deep-link into an application but you require your users to login first, this presents a problem. In an MVC application, this is commonly achieved using the build-in ReturnUrl parameter in the URL. For example, if a user tries to go to http://myapp/secure/page, MVC will redirect them to login and adjust the URL to http://myapp/account/login?ReturnUrl =%2fsecure%2fpage. The value of ReturnUrl is an encoded version of the path they initially attempted to access. The issue is that the mechanism inside MVC that handles the ReturnUrl sees that “/secure/page” is not an MVC route, and nulls out ReturnUrl.

Thankfully, this issue is fairly easy to fix. The ReturnUrl functionality is actually implemented in the Authorize attribute. All we have to do extend this class and provide the extra bit of functionality that it’s missing. Here is a code snippet:

public class AuthorizeAttributeSPA : System.Web.Mvc.AuthorizeAttribute
    {
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            base.OnAuthorization(filterContext);

            if (filterContext.Result is HttpUnauthorizedResult)
            {
                string returnUrl = "/";
                if (filterContext.HttpContext.Request.HttpMethod.Equals("GET", System.StringComparison.CurrentCultureIgnoreCase))
                {
                    returnUrl = filterContext.HttpContext.Request.RawUrl;
                }
                string url = string.Format("/Account/Login?ReturnUrl={0}", HttpUtility.UrlEncode(returnUrl));
                filterContext.Result = new RedirectResult(url);
            }
        }
    }

This custom attribute overrides the default behavior and handles the ReturnUrl parameter appropriately. Simply apply this attribute to the MVC controllers you want to secure, and deep links to protected pages will work.

And that’s it! I hope this information is helpful to you. As I mentioned earlier, there are several ways to solve these issues, and the techniques described in this post are the ones I’ve felt to be most effective. If you have other ways, please feel free to describe them in the comments section below. Thanks for reading, see you next time.

No Comments

Leave a Comment

Your email address will not be published. Required fields are marked *