Sunday 9 November 2014

Do Client-Side Routing with ui-router

I'm taking a little break from my mini-series on the input directives to talk about something a little different and a bit more interesting. Client side routing is a powerful concept, but it takes a little getting used to. We didn't originally use ui-router for our product as the decision had been made that we didn't need browser functionality (in particular back and forward) within our application. However, as the application grew it became apparent that we couldn't not have this functionality and so we investigated what was available in client side routing for us. We chose ui-router because it allowed us to have url rewriting, which allows our users to bookmark areas within our application as though they're separate pages on a standard website.

Worth Noting

It is worth noting that AngularJs 2.0 is reported to have far better routing than AngularJs 1.x has. I was listening to the Adventures in Angular podcast this week and they were talking about the new routing features coming up. If you are reading this after 2.0 has been released, then it's probably worth your while looking further into native routing rather than using ui-router. I'll make a note to blog about the new routing features as soon as I can lay my hands on them. In the meantime, ui-router is by far the best option in my opinion...

Why Use Routing

I think the best use case for client side routing is when you don't want to change the whole page content, but do want the user to be able to link to that section. For instance, you may have a header, footer, hero image, menu, etc., that you want to be displayed regardless of the main content in the page. In my opinion this is the best use case for ui-router. The functionality that it provides is quite powerful, you can even pass parameters between your states. I have created a search that on submit passed the search query to a new state and populated the main grid with search results, while in another state the main grid was populated by a category of products that the user had chosen from a menu.

Installing

ui-router is available on both Bower and Node, both of which know it as angular-ui-router. Use the relevant install command, or grab it from GitHub or a CDN, or whatever else works for you. From the root of your application use one of the following commands:

npm install angular-ui-router
bower install angular-ui-router

Setting Up Your App

JavaScript

Once you've referenced AngularJs and ui-router in your HTML file you need to tell your app about it using dependancy injection at the module level.

var myApp = angular.module('myApp', ['ui-router']);

Next you need to create the configuration. This is a typical configuration that I use. There are many other options and combinations, but this is my preferred basic version.

myApp.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider){
    $urlRouterProvider.otherwise('/');

    $stateProvider
        .state('stateOne', {
            url: '/stateOne',
            templateUrl: 'stateOne/index.html',
            controller: 'stateOneCtrl',
            data: {
                pageTitle: 'State one active'
            }
        })
        .state('stateTwo', {
            url: '/stateTwo',
            templateUrl: 'stateTwo/index.html',
            controller: 'stateTwoCtrl'
        })

        .state('stateThree.details', {
            url: '/stateThree/:id',
            templateUrl: 'stateThree/index.html',
            controller: 'stateThreeCtrl'
        });
}]);


Here we inject $stateProvider and $urlRouterProvider. These are both from ui-router and are the dependancies that replace AngularJs' own $routeProvider. Make sure that you use the array syntax that I have here if you are planning to minify your JavaScript files (you should).

The first thing the configuration should have is a default route. Here I've used "/", which means go to the top level URL relative to the page your app is registered on. If you do this in an HTML file in the root of your website then if would route to http://www.example.com/, but if you had your app's HTML file in another directory it would look like http://www.example.com/someDirectory/. This method also works if your HTML is embedded in a back end MVC solution. For instance, I've used ui-router as part of a .NET MVC 4 solution, where my app's root HTML was in Views/someDirectory/. If you have no default content you could use one of the states you later declare as a default route, or if it makes more sense for your user to be delivered to a particular page over another at the start of their journey. This is also the route that gets used if the user has entered a URL that doesn't exist, including due to typos, so make sure that it's not confusing to find oneself here.

Next, use $stateProvider to declare all of your states. In my example above I've included two, but there is no limit. This doesn't mean that you should go crazy, use as many states as your application needs and no more. Each .state corresponds to a single state in your application. The first parameter is the name of your state. Make this something clear, as you're going to be using this throughout your application to tell ui-router which state to transition to. The second parameter contains the configuration settings and options for that state.

For me, the starting options are url and templateUrl. url declares the URL that the browser is requesting, the URL that the user types into the address bar. templateUrl declares where on the server the HTML file that needs to be served. This HTML file should only be a partial HTML file, and not a fully formed document. The root element should be a block level container element such as div, article, or section. The alternative to templateUrl is template, which would then include you HTML inline like so:

template: '<div>Some HTML code goes in here</div>'

I don't think this is very tidy and it could mean delivering HTML code that is never used, so I don't use it nor recommend this method.

If you're going to use a controller you can declare it here with controller. The value here is the name of the controller that you've declared elsewhere in your AngularJs application, or a function that becomes the controller such as:

controller: function($scope){//controller code here}

I prefer to not use it as an inline function like this for the sake of clarity.

You can use URL parameters as a prettier version of query string parameters. This is convenient for the user as it allows them to bookmark a state with any variable information that the system needs to function. As a developer it makes accessing passed values much easier to reference in code. What you type after the colon (:) will be the variable name for the value that is passed in as that parameter.

There are many more options available, but these are the ones that I find most useful. I also use data to change things outside of the scope of the state's controller, but that's worth a post to itself. The next two options that I really want to work with are the onEnter and onExit callback functions. These clearly have great potential, but I've yet to use them in my current projects.

HTML

There's very little that needs to be done in the HTML in the way of setup. One thing you must do is add an ui-view attribute to declare where your content from the template for the current state should be inserted. I tend to put this on a main element, as whenever I've used ui-router I've only wanted to switch in and out the primary content on the page.

<main ui-view></main>

When the user navigates to a new state the content in the template option or in the HTML file at the location declared in the templateUrl option will be injected into this element. Any content that you put inside this element will be deleted when ui-router loads a state for the first time. I have been known to put default content in here, but beware that if it's not in a template then the user is never going to see it again once they've moved into a new state on the page. Every time a new state is loaded, ui-router removes the content of this element and injects the content that is associated with the new state.

Changing States

HTML

From the user's perspective a state change is the equivalent of moving between pages, even if the state is the same but reloading with new URL parameters. Due to this I think that all state changes must come due to some direct interaction the user has had with the page. The best way to do this is using click events which trigger some $scope function or using the a tag.

<a ui-sref="stateOne">Some indication as to where the user is being directed</a>

or if you're using URL parameters:

<a ui-sref="stateThree.details({id:2})">Some indication as to where the user is being directed</a>

When this is sent to the browser ui-router automatically populates the href attribute:

<a ui-sref="stateOne" href="#/stateOne”>Some indication as to where the user is being directed</a>

or when using parameters:

<a ui-sref="stateThree.details({id:2})" href="#/stateThree/2”>Some indication as to where the user is being directed</a>

When the user clicks on these the URL in their browser's address bar will look like this:

www.example.com/#/stateOne

or this:

www.example.com/#/stateThree/2

JavaScript

To change the state within your JavaScript you need to inject $state into your controller. $state is a service built into ui-router.

$state.go("stateOne")

or if you're using URL parameters add an object as a second parameter containing the values:

$state.go("stateThree", {id:2})

I'm always concerned when I'm using this method that I'm not utilising ui-router to the best affect. Perhaps this is where I should be using onEnter and onExit. There are two times where I have used this method.

The first, and the use case that I think is most justifiable for changing state in the JavaScript, is when I've used URL parameters that have needed checking before they could be used. If I've received bad data then I've redirected the user to either an error page or the home page.

The second occasion is when I've needed to pass user data as a URL parameter. I created a search function that used the same template as the category pages did, but a different controller. When the user submitted the form it triggered a function hanging off the $scope where I replaced any spaces with hyphens so that it was safe in the URL. Once the search controller grabs the value then I swap the hyphens back out for spaces before sending a request to the server. I know there are other options for this, but using hyphens allows users to easily create their own search terms directly in the address bar.

Retrieving URL parameters

Once your state change has been triggered you can retrieve the values passed as URL parameters in your controller. You need to inject $stateParams, which is built into ui-router, into your controller. At that point you can use dot notation to retrieve the values as passed, using the variable name that you declared in your configuration:

$stateParams.id

You'll get a better efficiency if you need to look at this value more than once by assigning it to a local variable and using that.

And beyond...

The documentation for ui-router is fantastic, and I've barely scratched the surface here. It's well worth reading through the ui-router wiki to see if there's any functionality that could help with your use case.

Not Having a # in the URL

I've tried to configure ui-router to not include # in the URLs, but I've had little success. To do this there is a certain amount of server side configuration, in my case IIS. There isn't much documentation out there on how to do this, and I've just plain failed to cobble it all together and reach a working solution. If you manage to do it, please let me know how.

No comments:

Post a Comment