Angular Routing using TypeScript


This is part of a blog series – please read the intro part and install the prerequisites if you want to follow along.

In angular you often work with SPAs (Single Page Applications) like our index.html. But what happens when you want to show the store details instead of the list of the list of stores?  Technically you could do something ultra-complex, showing and hiding elements left and right, but thankfully there is a better way – routing and views.

Change the markup

You know the drill by now. We’ll start by changing the HTML.

  1. In index.html replace the ng-include that includes the storeListView.html with an ng-view tag.
    <!-- content-->
    <ng-view></ng-view>
    

    The ng-view tag is just a place-holder and during routing we’ll tell it what to show here, i.e. the storeListView.html fragment or the storeDetailView.html fragment

  2. In storeListView.html remove the ng-controller part of the top div, we’ll inject it in the routing as well.
  3. In storeListView.html add a link around the store name to browse to the details section by store id
    <h4><a href="#storeDetails/{{store.StoreId}}">{{store.Name}}</a></h4>
    

Prepare to use Routing features

Angular is very modular so Routing, like Resource, is not part of the base Angular package which means that we need to do 3 things.

  1. Add the angular-route library (using bower.json or the bower package manager)
  2. Add the lib/angular-route/angular-route.js script to index.html
  3. Add the angular-route typescript definition with "tsd install angular-route --save" in the package manager console

Add the routing code

This is what we are trying to accomplish

  1. when someone browses to index.html#/storeList – show storeListView.html (in the ng-view) and use the controller StoreListCtrl
  2. when someone browses to index.html#/storeDetails/1 (or some other number) – show storeDetailView.html and use the controller StoreDetailCtrl
  3. whenever someone browses to something else – just browse to index.html#/storeList

in order to do this we need to add configuration to the mallApp module (in app.ts)

main.config(routeConfig);
 
routeConfig.$inject = ["$routeProvider"];
function routeConfig($routeProvider: ng.route.IRouteProvider): void{
   $routeProvider
      .when("/storeList", {
         templateUrl: "/templates/storeListView.html",
         controller: "StoreListCtrl as vm"
      })
      .when("/storeDetails/:storeId", {
         templateUrl: "/templates/storeDetailView.html",
         controller: "StoreDetailCtrl as vm"
      })
      .otherwise("/storeList");
}

The code is pretty straight-forward. Call .when to set up routes and .otherwise for the default route.

The URLs get a bit ugly, I won’t get into how to prettify them here, but this is one way https://scotch.io/quick-tips/pretty-urls-in-angularjs-removing-the-hashtag note though that you need to configure the server to work with HTML5 mode.

I should note that there is another common way to route in angular as well and that is with the ui-router.  With the UI router you route based on state rather than just the route URL. With ui-router you can have nested views, so you could have a different partial for a side column or the header but for the purposes of this article we'll use the ngRoute.

Before we browse to the page we need to inject ngRoute into the mallApp module (in app.ts) so the mallApp understands about routing

var main = angular.module("mallApp", ["ngRoute", "common.services"]);

Browse the site

Browse to index.html and you should see the same store listing as before, but notice the address bar.  The URL has now changed to index.html#/storeList because of our .otherwise route.

If you click on one of the store names you will see how the view is replaced by a details view - even if the databinding is not working since we haven't implemented the StoreDetailCtrl yet - but here you should also notice how the URL has changed to point to /storeDetails/...

Implement the StoreDetailCtrl

Ok, what do we need the Store Detail Controller to expose?  Let's have a look at the HTML for storeDetailView.html

This section for the tab section on the right has a couple of interesting angular directives. The ng-click directive lets us perform an action on click, so in this case we'll call a method setTab on the controller to activate the tab the user clicked on.

In order to indicate that the tab is active we need to make it use the "active" css class and angular provides a neat way to set this using the ng-class directive.  Here we set it to active if the proper tab is set so we need to expose a field tab in the controller.

<ul class="nav nav-tabs">
   <li ng-click="vm.setTab(0)" ng-class="(vm.tab == 0) ? 'active' : ''"><a>Hours</a></li>
   <li ng-click="vm.setTab(1)" ng-class="(vm.tab == 1) ? 'active' : ''"><a>Contact</a></li>
   <li ng-click="vm.setTab(2)" ng-class="(vm.tab == 2) ? 'active' : ''"><a>Location</a></li>
</ul>

Of course marking the tab active is a nice UI thing but it means nothing unless we change the content of the tab, and we do this by showing or hiding content with ng-show

<div class="tab-content" ng-show="(vm.tab == 0)">

The rest of the databindings are pretty similar to what we are used to from before, but essentially they all bind to data in the store object.

  1. Create a new StoreDetailController.ts in app/stores and add the following code to define the interface for IStoreDetailController

    module app.stores {
        interface IStoreDetailController {
            store: app.domain.IStore;
            tab: number;
            setTab(value: number): void;
        }
    }
    
  2. We need to grab the parameters from the URL - the route parameters - in this case storeId, and to do that we need to define IStoreParams.  Note that storeId here must match the route parameter name from /storeDetails/:storeId
    interface IStoreParams extends ng.route.IRouteParamsService {
       /* must match the route param name*/
       storeId: number;    
    }
    
  3. Next we are going to implement the StoreDetailController.
    class StoreDetailController implements IStoreDetailController {
       store: app.domain.IStore;
       tab: number;
    
       static $inject = ["$routeParams", "dataAccessService"];
       constructor(private $routeParams: IStoreParams, private dataAccessService: app.common.services.DataAccessService) {
          this.tab = 0;
    
          //get the value from the route
          var storeId = $routeParams.storeId;
    
          var storeResource = dataAccessService.getStoreResource();
          //the param name i.e. "id" must match the api parameter name
          storeResource.get({ id: storeId }, (data: app.domain.IStore) => {
             this.store = data;
          });
       }
    
       setTab(value: number): void {
          this.tab = value;
       }
    }
    

    Here the route parameters are injected to the constructor along with the dataAccessService.
    Since we want a single item we'll call the .get method on the resource and in this case, the parameter (id) that we pass in, needs to match the parameter name of the API [HttpGet("{id}")] so this is by no means random.

  4. Finally, to bring it all together, we need to register the controller with the mallApp module.
    angular.module("mallApp").controller("StoreDetailCtrl", StoreDetailController);
    
  5. Oh... before I forget... you need to include the script for the StoreDetailController in index.html of course
    <script src="app/stores/StoreDetailController.js"></script>
    

With all that in place we are now ready to marvel at the beauty of our app.  We are officially done with the walkthrough and by now you should hopefully have a nice skeleton to start out with for your Angular/TypeScript endeavours...

Next: Bonus - Resource Mocking

Comments (0)

Skip to main content