Angular Routing
Objectives
- Incorporate an organized folder structure into an Angular app
- Use a configuration block to add routing to an app
- Retrieve route params from within a controller
- Compare hashbang mode to HTML5 mode when configuring the router
Note, we'll be building upon the Star Wars app from the Angular Services codealong
Refactoring the Star Wars App
You'll probably notice soon that placing all of our controllers and services into app.js
will soon become disorganized. So let's refactor the Star Wars app from earlier. Let's follow this layout.
- app
- app.js
- controllers.js
- services.js
- views
- index.html
This will work for our examples in class, but for more complex apps, you will want to look at file structures that compartmentalize your app even more. See this Scotch tutorial for details
Now, let's move everything out of app.js
so that we have three modules. We'll be injecting these modules like so:
- inject ngResource into StarWarsServices
- inject StarWarsServices into StarWarsCtrls
- inject StarWarsCtrls into StarWarsApp
Whew! That's a lot of dependency injection. While beyond the scope of our Angular unit, injecting dependencies makes testing in Angular easier, because we can mock different dependencies if needed by defining test dependencies (this would be very useful for "faking" API calls).
app.js
angular.module('StarWarsApp', ['StarWarsCtrls'])
controllers.js
angular.module('StarWarsCtrls', ['StarWarsServices'])
// place your controllers down here
services.js
angular.module('StarWarsServices', ['ngResource'])
// place your services down here
Also, make sure to change and include those script files in index.html
<script src="app/app.js"></script>
<script src="app/services.js"></script>
<script src="app/controllers.js"></script>
Moving on, let's add routing to our app.
Using the AngularUI Router
We'll be using the popular ui.router
module for routing our application.
The CDN link you'll need in index.html
:
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.18/angular-ui-router.js"></script>
You'll probably notice that this is a dependency, and it'll need to be injected into our app.
In app/app.js
:
var app = angular.module('StarWarsApp', ['StarWarsCtrls', 'ui.router']);
And lastly, we'll need a way to switch out templates and controllers on our index page. For now, let's replace the contents of <body></body>
with the following:
In index.html
:
<body>
<div class="container">
<div ui-view></div>
</div>
</body>
The ui-view
directive will help complement our routes by including the template and controller of the current route. Whenever a route changes, the contents of ui-view
are replaced with the new template and controller.
But we need routes! And controllers! And views! We already have the controller, so let's set up the view. In app/views
, we'll create a template called films.html
to list out all the films.
`app/views/films.html
<h1>Star Wars Films</h1>
<div class="well" ng-repeat="film in films">
<h2>{{film.title}}</h2>
<p>{{film.opening_crawl}}</p>
</div>
Configuring Routes
In app/app.js
, let's add the following to the bottom of the file:
.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/');
//define routes
$stateProvider
.state('films', {
url: '/',
templateUrl: 'views/films.html',
controller: 'FilmsCtrl'
});
//$locationProvider.html5Mode(false).hashPrefix('!');
}]);
Note: Run the app using a local HTTP server.
To add additional routes, just add additional state
functions. Remember to define a controller (if necessary) for each page and create content in the related template files. Adding the otherwise
condition is handy for redirecting invalid routes.
.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/');
//define routes
$stateProvider
.state('films', {
url: '/',
templateUrl: 'views/films.html',
controller: 'FilmsCtrl'
})
.state('about', {
url: '/about',
templateUrl: 'views/about.html'
});
}]);
Try it: Take a moment and implement a template for an about page.
Linking to routes
You can link to angular routes using <a>
link tags
<a href="/#/about">About Page</a>
Wait, what's this prefix?
You'll see that Angular routes contain a "hashbang" prefix #. This is necessary in order to distinguish between front-end and back-end routes. A route like /about
would end up being routed to a server, while Angular knows that /#/about
is a front-end route.
We can configure our app to not use the "hashbang", but it will involve some configuration on the server. Specifically for Express, you would want all routes that aren't the root route (/
) to send the back index.html
.
See this StackOverflow response for more on this topic. We'll be implementing this configuration later on.
Passing URL Parameters
Passing URL parameters with angular routes is similar to routes in Express or Rails. First we define a route with a parameter.
.state('showFilm', {
url: '/films/:id',
templateUrl: 'views/filmShow.html',
controller: 'FilmShowCtrl'
})
Then, we can access the parameter in the controller using $stateParams
(which must be injected into the controller). So let's add another controller in controllers.js
for getting a specific film.
.controller('FilmShowCtrl', ['$scope', '$stateParams', 'Films', function($scope, $stateParams, Films) {
$scope.film = {};
Films.get({id: $stateParams.id}, function success(data) {
$scope.film = data;
}, function error(data) {
console.log(data);
});
}])
Now we need a filmShow.html
file in the views
folder:
<h1>{{film.title}}</h1>
<div class="well">
<p>{{film.opening_crawl}}</p>
<small>Release date: {{film.release_date}}</small>
</div>
<a href="/#/" class="btn btn-primary">← Back</a>
...andddddd we need to add links to views/films.html
<h1>Star Wars Films</h1>
<div class="well" ng-repeat="film in films track by $index">
<h2><a ng-href="/#/films/{{$index+1}}">{{film.title}}</a></h2>
</div>
Note: Why do we use ng-href
? Because Angular (see documentation)
Programatically Navigating
You can also cause the angular router to navigate to a new route using $state
(another service).
.controller('FilmsCtrl', ['$scope', '$state', 'Films', function($scope, $state, Films) {
// $scope.films = [];
//
// Films.query(function success(data) {
// $scope.films = data;
// }, function error(data) {
// console.log(data);
// });
//this will redirect to a different state
$state.go('about');
}])
This can also be used to get the current state, simply by leaving the parenthesis empty.
//returns the current angular state, complete with the URL, template, and controller
$state.current
Resources
- Nested State in UI Router
- ngRoute
- The official Angular router, with less functionality than UI router