Table of contents

< Previous chapterNext chapter >

Last updated .

Navigation on the client side

If you've worked with ASP.Net MVC, you're used to the concept of routing. In ASP.Net MVC, routing is the mechanism whereby requests (urls) are mapped - routed - to controllers. What typically happens in MVC when a controller is called is that it processes the data in some way and then generates a view, i.e. a fresh html response is sent to the browser. Routing in the context of Angular is a somewhat similar concept, but it is essentially all client-side. Routing is what you use when navigating between pages, only with Angular there is just one page, the contents of which are changed dynamically without a complete page reload. Although such a single-page app can be constructed using just javascript and Ajax, Angular is supposed to streamline it for us.

One could keep everything in one html structure and then just show or hide parts of it selectively, depending on context. But that would quickly get out of control as the page grows in size and complexity and more subpages are added. Instead, it's preferable to define a single container page and then swap part of the contents to produce a different view, getting the html for the view from the server with a behind-the-scenes http request. If you've worked with ASP.Net, this is similar to master pages in ASP.Net Webforms or layout pages in ASP.Net MVC, only in Angular, the assembly of the view is done on the client side. In Angular, such as master page ("template") is termed a layout template.

A potential problem with such an approach is that the current url of the page doesn't change when the view changes, thus violating the basic web principle that the url displayed should reflect what is viewed in browser. In practice, this could ruin the functionality of the browser back and forward buttons and make bookmarks point to unexpexted content, which would put the user off. This is all handled gracefully, though, by the Angular routing mechanism, enabling what is called deep linking.

Routing is done by using the $route service to wire together controllers and view templates. When the user navigates to another view, the service connects that view with a new controller. A view template is represented with the ngView directive.

To get going with a minimal application to test routing, I have tried to come up with a good example app to showcase it. Imagine an application with a simple home page. From there, one can navigate to another page showing a list of contacts, each of which displays a link, among other things. Clicking on the link takes the user to a third page, showing details for the item. page that shows a list of contacts. In other words, a "master-detail" view. All this is going to take place in the same page - no page reload takes place.

The basic app skeleton

Before I dive into the challenge of implementing routing, I want to get the basic app structure straight. For this demo, I decided to start on a more appropiate partitioning of components into files and folders, because this example is going to involve more files than previously. Each feature (whatever that is) should really be represented by its own folder. First, I moved the basic script that creates the root Angular module into a /App/App.js file:

App/App.js ( function () { // Create root module var app = angular.module('AngularProject', []); } )();

Next, I will need three templates, two of which with a corresponding controller in a separate script file. First, the landing page, which goes right in the project root folder. This time there will be no need for a controller in the main page, because only the subpages will need that. I hope all of this will look familiar to you, if you have followed along in the previous chapters.

HomePage.html <!doctype html> <html ng-app="AngularProject"> <head> <title>Home</title> <meta charset="utf-8" /> <script src="Scripts/Angular/angular-1.5.0-rc.0/angular.js"></script> <script src="Scripts/Angular/angular-1.5.0-rc.0/angular-route.js"></script> <style> ul { list-style:none; } </style> </head> <body> <h1>The Contact List App</h1> <div ng-view></div> <script src="App/App.js"></script> <script src="Home/HomeController.js"></script> <script src="Contact/ContactsController.js"></script> </body> </html>

This HomePage page also represents the layout page. I have added a div with an ng-view directive, which functions as a placeholder for content. The two html files that I will list next will only contain markup snippets to be injected into the layout page at the place of the tag with the ng-view directive. First, a template for the default content. This template is meant to be shown on the landing page when it is first requested.

Home/Home.html <h1>{{ vm.Header }}</h1> <a href="#contacts">Show Contact List</a>

The simple controller just contains a simple message:

Home/HomeController.js ( function () { var app = angular.module('AngularProject'); app.controller("homeController", HomeController, []); function HomeController() { var vm = this; vm.Header = "Welcome to home!"; } } )();

The page can now be viewed. I suggest you try it, although it is not that interesting, showing only a header.

Enabling routing

Right now the home page loads just fine, displaying a message, but there is no content, so let's fix that first. Routing involves using ng-view directives, as shown above and a $routeprovider. The $routeprovider service is used for configuring the routing. It is not part of the standard Angular package; a separate script file must be referenced. I did not have to download or install anything, because the file was already there in the zip file that I downloaded at the outset of this series. As you can see in the markup for the HomePage.html file above, I have already added a script tag for the angular-route.js file.

To set up the routing in the root module I had to modify the App/App.js script file:

App/App.js ( function () { // Create root module var app = angular.module('AngularProject', ['ngRoute']); app.config(function ($routeProvider) { $routeProvider .when('/', { templateUrl: 'home/home.html', controller: 'homeController', controllerAs: 'vm', }) .otherwise({ redirectTo: '/' }); }); } )();

We now depend on the route provider, which is made available by indicating the dependency with the ['ngRoute'] construct. Then, in the call to the config function of the root module, the routing can be defined. In this case, there is only one route for the "home" template. I also added a fallback rule using the otherwise function , so that unknown paths are redirected to the home template.

Let's see what happens when we view the home page. Up till now, I have just been requesting the html files directly from the file system, but now I will have to go through IIS Express. That is because Angular will be making request for files and will be denied access to the file system. So, I configured Visual Studio to request the HomePage.html file on startup. And I pressed F5 in Visual Studio and got the right kind of response. As you can see, the view is the result of a merger of the HomePage.html file with the markup snippet from Home/Home.html, prooving that the routing that we set up is working. Now, there are a couple of things to note: 1) the browser url is now [baseaddress]/HomePage.html#/: The last two characters have been added by the Angular routing. 2) Clicking on the link requests http://localhost:56585/HomePage.html#contacts, but nothing happens and the url in the browser address bar stays the same. This is due to the fallback rule set up in the routing; the url is unknown and so the homepage is requested, which is the page we are already viewing.

The Home template
The Home template

Notice there is nothing in the markup (Home/Home.html) that specifies a controller. The controller (Home/HomeController.js) gets attached to this DOM section due to the routing we set up.

Adding a template

Right now, there is a dead link on the HomePage.html template pointing to a non-exising #contacts resource. So, let us add the view and controller for a contacts list:

Contact/Contacts.html <h2>{{ vm.Header }}</h2> <ul ng-controller="contactsController as vm"> <li ng-repeat="contact in vm.Contacts">{{ contact.Name }}</li> </ul>

The template is capable of showing a list of records. Now, the controller to back up that template:

Contact/ContactsController.html ( function () { var app = angular.module('AngularProject'); app.controller("contactsController", ContactsController, []); function ContactsController() { var vm = this; vm.Header = "List of all contacts"; vm.Contacts = [ { Name: "Albert Einstein", Email: "alei@princeton.org", Phone: "1 999 293 321", Birthday: new Date(1879, 2, 14) }, { Name: "Stephen Hawking", Email: "shaw@cambridge.org", Phone: "45 233 2321", Birthday: new Date(1942, 0, 8) }, { Name: "Niels Bohr", Email: "nibo@nbinstitute.ku.dk", Phone: "45 38 23 11", Birthday: new Date(1885, 9, 7) }, { Name: "Leonard Susskind", Email: "lsus@princeton.org", Phone: "1 238 923 119", Birthday: new Date(1940, 4, 20) }, ]; } } )();

In the controller, the data for the contact list are hardcoded, but try to imagine them dynamic and fetched with a service call. Finally, we need to add support for the new resource to the routing:

App/App.js ( function () { // Create root module var app = angular.module('AngularProject', ['ngRoute']); app.config(function ($routeProvider) { $routeProvider .when('/', { templateUrl: 'home/home.html', controller: 'homeController', controllerAs: 'vm', }) .when('/contacts', { templateUrl: 'contact/contacts.html', controller: 'contactsController', controllerAs: 'vm', }) .otherwise({ redirectTo: '/' }); }); } )();

Again, lets view the new version in the browser. Now, when the Show contact list link is clicked, the view changes to display the list of the four contacts. In other words, the Home.html template has been swapped with the Contact/Contacts.html template, as expected. Notice that the url in the browser address bar also changes to http://localhost:56585/HomePage.html#/contacts to reflect the new view, which is good. Hitting the browser refresh button causes a full page reload, but still showing the same subpage. That is also well. Clicking on the browser back button takes the user back to view the default Home.html template. Everything works.

The contact list template
The contact list template

Using clean urls

You have probably noticed that the urls employed in the routing embed "#" characters. This is considered harmful, although I can't say I understand why. But, anyway, in case you'd want to get rid of the hashtags, it requires using html5mode. And this requires just 3 changes to the code:

Add a call to $locationProvider.html5Mode in App/App.js:

App/App.js ( function () { // Create root module var app = angular.module('AngularProject', ['ngRoute']); app.config(function ($routeProvider, $locationProvider) { $routeProvider .when('/', { templateUrl: 'home/home.html', controller: 'homeController', controllerAs: 'vm', }) .when('/contacts', { templateUrl: 'contact/contacts.html', controller: 'contactsController', controllerAs: 'vm', }) .otherwise({ redirectTo: '/' }); $locationProvider.html5Mode(true); }); } )();

Then, in the HomePage.html file, add a base tag, like this:

HomePage.html (partial) <head> ... <base href="/"> ... </head>

And finally, get rid of the hashtag in the link:

Contact/Contacts.html <h1>{{ vm.Header }}</h1> <a href="contacts">Show Contact List</a>

Running the same manual tests in the browser, the navigation still works fine and so do the browser back and forward buttons. There is just the problem that when I hit the browser refresh button, I get a HTTP error 404, because the browser has requested something like http://localhost:56585/contacts". Not good. The problem is there is no way the browser can know that this is not a real server-side resource. As the Angular documentation says, using the html5mode "requires URL rewriting on server side". But a good workaround exists. In terms of ASP.Net, it means serving the default html file for all requests of a non-existing resource. This can be done in a global.asax as such:

Global.asax using System; namespace AngularProject { public class Global : System.Web.HttpApplication { private const string ROOT_DOCUMENT = "/Homepage.html"; protected void Application_BeginRequest(Object sender, EventArgs e) { string url = Request.Url.LocalPath; if (!System.IO.File.Exists(Context.Server.MapPath(url))) Context.RewritePath(ROOT_DOCUMENT); } } }

Now, the browser refresh button works again.

Transmitting data via the route

Often, you will want to send data along when navigating to a new view, for example an ID of a selected item. This can be done by embedding a named parameter in the url of a link and then configure the route to accept a parameter of such a name. To illustrate sending a parameter, I have added a new template to show the contact info for a contact. The idea is that when a user clicks on a contact name in the list of contacts, he is taken to a detail view showing various data. This is my markup snippet for that view:

Contact/Contact.html <h1>{{ vm.Header }}</h1> <ul> <li>The man's email address: {{ vm.Contact.Email }} </li> </ul>

As you can see, it will show just the email address of the selected contact. Next, a controller for that view:

Contact/ContactController.js ( function () { var app = angular.module('AngularProject'); app.controller("contactController", ContactController, ['$routeParams']); function ContactController($routeParams) { var vm = this; vm.Header = "Contact detail"; vm.Contact = { Email: $routeParams.email, }; } } )();

This controller depends on $routeParams, which is an object that will contain the routed parameters, in this case a parameter named "email".

Next, I modified the contact list to be a list of links that can bring the user to the detail view. Note that the email is now a parameter in the link url:

Contact/Contacts.html <h2>{{ vm.Header }}</h2> <ul ng-controller="contactsController as vm"> <li ng-repeat="contact in vm.Contacts"> <a ng-href="contact/{{ contact.Email }}">{{ contact.Name }}</a> </li> </ul>

I also need to update the routing to account for the new view. A parameter is defined with the ':' character:

App/App.js ( function () { var app = angular.module('AngularProject', ['ngRoute']); app.config(function ($routeProvider, $locationProvider) { $routeProvider .when('/', { templateUrl: 'home/home.html', controller: 'homeController', controllerAs: 'vm', }) .when('/contacts', { templateUrl: 'contact/contacts.html', controller: 'contactsController', controllerAs: 'vm', }) .when('/contact/:email', { templateUrl: 'contact/contact.html', controller: 'contactController', controllerAs: 'vm', }) .otherwise({ redirectTo: '/' }); $locationProvider.html5Mode(true); }); } )();

And finally, I need to reference the new script file in the main page. This is the complete file:

HomePage.html <!doctype html> <html ng-app="AngularProject"> <head> <title>Home</title> <meta charset="utf-8" /> <script src="Scripts/Angular/angular-1.5.0-rc.0/angular.js"></script> <script src="Scripts/Angular/angular-1.5.0-rc.0/angular-route.js"></script> <style> ul { list-style:none; } </style> <base href="/"> </head> <body> <h1>The Contact List App</h1> <div ng-view></div> <script src="App/App.js"></script> <script src="Home/HomeController.js"></script> <script src="Contact/ContactsController.js"></script> <script src="Contact/ContactController.js"></script> </body> </html>

Now, I can navigate from the start page to the contact list and from there to the details of the contact (in this case showing just the email address). Notice that the parameter is appended in the url:

Home.html
Home.html
Contacts.html
Contacts.html
Contact.html
Contact.html

If you want to do some more experiments from here, you can download the source files. Make your own Visual Studio project to host them.

< Previous chapterNext : Interacting with a server >


2016 by Niels Hede Pedersen Linked in