Routing & Multiple Views

Note: If you skipped ahead to this section, click here to download a zip of the tutorial at this point.

In this step, you will learn how to create a layout template and how to build an app that has multiple views by adding routing, using an Angular 1 module called ui-router.

The goals for this step:

  • When you navigate to index.html, you will be redirected to /parties and the party list should appear in the browser.
  • When you click on a party link the URL should change to one specific to that party and the stub of a party detail page is displayed.

Dependencies

The routing functionality added by this step is provided by the ui-router module, which is distributed separately from the core Angular 1 framework.

Type in the command line:

meteor npm install --save angular-ui-router
5.1 Add ui-router package.json
8
9
10
11
12
13
14
  "dependencies": {
    "angular": "^1.5.3",
    "angular-meteor": "^1.3.9",
    "angular-ui-router": "^0.2.18",
    "meteor-node-stubs": "~0.2.0"
  },
  "devDependencies": {

Then add the ui-router as a dependency to our Socially app in socially.js:

5.2 Add uiRouter to Socially imports/ui/components/socially/socially.js
1
2
3
4
5
6
 
12
13
14
15
16
17
18
import angular from 'angular';
import angularMeteor from 'angular-meteor';
import uiRouter from 'angular-ui-router';
 
import template from './socially.html';
import { name as PartiesList } from '../partiesList/partiesList';
...some lines skipped...
// create a module
export default angular.module(name, [
  angularMeteor,
  uiRouter,
  PartiesList
]).component(name, {
  template,

Multiple Views, Routing and Layout Template

Our app is slowly growing and becoming more complex. Until now, the app provided our users with a single view (the list of all parties), and all of the template code was located in the main.html file.

The next step in building the app is to add a view that will show detailed information about each of the parties on our list.

To add the detailed view, we could expand the main.html file to contain template code for both views, but that would get messy very quickly.

Instead, we are going to turn the index.html template into what we call a "layout template". This is a template that is common for all views in our application. Other "partial templates" are then included into this layout template depending on the current "route" — the view that is currently displayed to the user.

Application routes in Angular 1 are declared via the $stateProvider, which is the provider of the $state service. This service makes it easy to wire together controllers, view templates, and the current URL location in the browser. Using this feature we can implement deep linking, which lets us utilize the browser's history (back and forward navigation) and bookmarks.

Template

The $state service is normally used in conjunction with the uiView directive. The role of the ui-view directive is to include the view template for the current route into the layout template. This makes it a perfect fit for our main.html file.

Now let's go back to index.html to add base tag to our main html file:

5.3 Add base tag to main template client/index.html
1
2
3
4
5
6
<head>
  <base href="/">
</head>
<body ng-app="socially" ng-strict-di="">
  <socially></socially>
</body>

We still need to add uiView directive, Socially is the best place for it:

5.4 Add uiView to Socially view imports/ui/components/socially/socially.html
1
<div ui-view=""></div>

Let's define a default route and use html5 mode to make urls look a lot fancier:

5.6 Set html5Mode and `parties` as default route imports/ui/components/socially/socially.js
18
19
20
21
22
23
24
25
26
27
28
29
30
  template,
  controllerAs: name,
  controller: Socially
})
  .config(config);
 
function config($locationProvider, $urlRouterProvider) {
  'ngInject';
 
  $locationProvider.html5Mode(true);
 
  $urlRouterProvider.otherwise('/parties');
}

It would be nice to have a navigation. Create one! Let's call our new component Navigation:

5.7 Create view for Navigation component imports/ui/components/navigation/navigation.html
1
2
3
<h1>
  <a href="/parties">Home</a>
</h1>
5.8 Create Navigation component imports/ui/components/navigation/navigation.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import angular from 'angular';
import angularMeteor from 'angular-meteor';
 
import template from './navigation.html';
 
const name = 'navigation';
 
// create a module
export default angular.module(name, [
  angularMeteor
]).component(name, {
  template,
  controllerAs: name
});

And implement it in Socially:

5.9 Implement Navigation in the view imports/ui/components/socially/socially.html
1
2
3
<navigation></navigation>
 
<div ui-view=""></div>
5.10 Add Navigation to Socially imports/ui/components/socially/socially.js
4
5
6
7
8
9
10
 
14
15
16
17
18
19
20
21
 
import template from './socially.html';
import { name as PartiesList } from '../partiesList/partiesList';
import { name as Navigation } from '../navigation/navigation';
 
class Socially {}
 
...some lines skipped...
export default angular.module(name, [
  angularMeteor,
  uiRouter,
  PartiesList,
  Navigation
]).component(name, {
  template,
  controllerAs: name,

Notice we did 3 things:

  1. Replaced all the content inside Socially component with ui-view (this will be responsible for including the right content according to the current URL).
  2. Defined default route.
  3. Created navigation.
  4. We also added a base tag in the head (required when using HTML5 location mode - would be explained a bit further).

Note that you can remove main.html now, because it's no longer in use!

Routes definition

Now let's configure our routes. There are no states at this stage, so let's add parties inside PartiesList component:

1
2
3
4
5
6
 
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import angular from 'angular';
import angularMeteor from 'angular-meteor';
import uiRouter from 'angular-ui-router';
 
import template from './partiesList.html';
import { Parties } from '../../../api/parties';
...some lines skipped...
// create a module
export default angular.module(name, [
  angularMeteor,
  uiRouter,
  PartyAdd,
  PartyRemove
]).component(name, {
  template,
  controllerAs: name,
  controller: PartiesList
})
  .config(config);
 
function config($stateProvider) {
  'ngInject';
  $stateProvider
    .state('parties', {
      url: '/parties',
      template: '<parties-list></parties-list>'
    });
}

And we will also add a state for a new page that will display the party details.

Our application routes are defined as follows:

  • ('/parties'): The parties list view will be shown when the URL hash fragment is /parties. To construct this view, Angular 1 will use the parties-list Component.
  • ('/parties/:partyId'): The party details view will be shown when the URL hash fragment matches '/parties/:partyId', where :partyId is a variable part of the URL. To construct the party details view, Angular will use the party-details Component.
  • $urlRouterProvider.otherwise('/parties'): Triggers a redirection to /parties when the browser address doesn't match either of our routes.
  • $locationProvider.html5Mode(true): Sets the URL to look like a regular one. More about it here.
  • Each template is just a regular usage of our components.

Note the use of the :partyId parameter in the second route declaration. The $state service uses the route declaration — /parties/:partyId — as a template that is matched against the current URL. All variables defined with the : notation are passed into the Component through the $stateParams object.

Components

I mentioned a state with party details. We have to define our partyDetails component.

Let's create the view for this Component in a new file:

5.11 Create view for the PartyDetails imports/ui/components/partyDetails/partyDetails.html
1
The party you selected is: {{ partyDetails.partyId }}

Now we can create actual component:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import angular from 'angular';
import angularMeteor from 'angular-meteor';
import uiRouter from 'angular-ui-router';
 
import template from './partyDetails.html';
 
class PartyDetails {
  constructor($stateParams) {
    'ngInject';
 
    this.partyId = $stateParams.partyId;
  }
}
 
const name = 'partyDetails';
 
// create a module
export default angular.module(name, [
  angularMeteor
]).component(name, {
  template,
  controllerAs: name,
  controller: PartyDetails
});

And also define new route which was mentioned before as /parties/:partyId:

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 
// create a module
export default angular.module(name, [
  angularMeteor,
  uiRouter
]).component(name, {
  template,
  controllerAs: name,
  controller: PartyDetails
})
  .config(config);
 
function config($stateProvider) {
  'ngInject';
 
  $stateProvider.state('partyDetails', {
    url: '/parties/:partyId',
    template: '<party-details></party-details>'
  });
}

Now let's add a link from each party in the parties list to it's details page:

5.14 Add PartyDetails to Socially imports/ui/components/socially/socially.js
4
5
6
7
8
9
10
 
16
17
18
19
20
21
22
 
import template from './socially.html';
import { name as PartiesList } from '../partiesList/partiesList';
import { name as PartyDetails } from '../partyDetails/partyDetails';
import { name as Navigation } from '../navigation/navigation';
 
class Socially {}
...some lines skipped...
  angularMeteor,
  uiRouter,
  PartiesList,
  PartyDetails,
  Navigation
]).component(name, {
  template,
2
3
4
5
6
7
8
9
10
 
<ul>
  <li ng-repeat="party in partiesList.parties">
    <a ui-sref="partyDetails({ partyId: party._id })">
      {{party.name}}
    </a>
    <p>{{party.description}}</p>
    <party-remove party="party"></party-remove>
  </li>

Now all is in place. Run the app and you'll notice a few things:

  • Click on the link in the name of a party - notice that you moved into a different view and that the party's id appears in both the browser's url and in the template.
  • Click back - you are back to the main list, this is because of ui-router's integration with the browser's history.
  • Try to put arbitrary text in the URL - something like http://localhost:3000/strange-url. You should to be automatically redirected to the main parties list.

Summary

With the routing set up and the parties list view implemented, we're ready to go to the next step and implement the party details view.