Update: My next post greatly improves on this example. I recommend starting there.

Setting the document title in an Angular2 app isn’t as straightforward as simply binding a property to the HTML <title> tag. Since an Angular app lives within the <body> of a DOM, Angular2 has no way to bind properties into <head> where the title is located.

The solution is documented nicely for Angular2 Typescript, but the Dart documentation for the same is pending. In this blog, I’ll explain how to perform this basic task in Angular2 Dart. If you’re looking for the short answer, skip ahead to Injecting the Title Service or straight to the source code.

So this post doesn’t become entirely irrelevant once the Dart documentation is written, I will further illustrate one way in which title changes can be tied to routing changes with a start-to-finish example.

Solution Overview

We know Javascript provides a direct way to change a doc title (e.g. document.title="No Sweat!") which we could emulate, but we would lose abstraction which could make it difficult to run the app in a non-browser environment someday. Instead, Angular provides a service which provides an API for the rather unpretentious task of getting/setting a document title. If we rely on this API, we can be sure that wherever the app runs, whatever represents a title will be accordingly updated when we want.

Preparation

This demonstration is built on a basic bare-bones Angular2 app (generated from stagehand). Read the quickstart if you need help getting up and running.

The stagehand template provides us a pubspec.yaml sufficient for our project and an AppComponent serving as starting point.

A Simple App with Routing

Because I like baseball, this simple app will show baseball players, teams, and fields, using a separate component to display each. Additionally, we allow users to tap a player name to view more detailed info about that player.

To begin, we create three very simple and nearly-identical components: PlayersComponent, TeamsComponent, FieldsComponent. Here’s FieldsComponent:

import 'package:angular2/core.dart';

@Component(
    selector: 'my-fields',
    template: '<h2>Fields</h2>'
)
class FieldsComponent {
  FieldsComponent();
}

We want AppComponent to route users to these three components, so we update the main AppComponent to include a RouteConfig that provides easy navigation:

import 'package:angular2/core.dart';
import 'package:angular2/router.dart';
import 'package:angular_dart_page_titles_on_route/page/fields_component.dart';
import 'package:angular_dart_page_titles_on_route/page/players_component.dart';
import 'package:angular_dart_page_titles_on_route/page/teams_component.dart';

@Component(
    selector: 'my-app',
    styleUrls: const ['app_component.css'],
    templateUrl: 'app_component.html',
    directives: const [ROUTER_DIRECTIVES],
    providers: const [ROUTER_PROVIDERS],
    )
@RouteConfig(const [
  const Route(path: '/players', name: 'Players', component: PlayersComponent, useAsDefault: true),
  const Route(path: '/teams', name: 'Teams', component: TeamsComponent),
  const Route(path: '/fields', name: 'Fields', component: FieldsComponent),
])
class AppComponent {}

Now that the routes are paved, we update AppComponent’s HTML to show navigation links and an outlet for routed components:

<nav>
  <ul>
    <li><a [routerLink]="['Players']">Players</a></li>
    <li><a [routerLink]="['Teams']">Teams</a></li>
    <li><a [routerLink]="['Fields']">Fields</a></li>
  </ul>
</nav>

<div class="content" id="content">
  <router-outlet></router-outlet>
</div>

If we run the project now, we have a simple menu with three items, which we can navigate amongst to show placeholder pages with headings.

Let’s update the app so that we can drill down to specific players.

First, we create a PlayerDetailComponent that will handle display of individual players.

import 'package:angular2/core.dart';
import 'package:angular2/router.dart';

@Component(
    selector: 'my-player-detail',
    template: '''<h2>Player Detail</h2> <h3></h3>''',
)
class PlayerDetailComponent implements OnInit {
  final RouteParams _routeParams;
  String player = "Not selected";

  PlayerDetailComponent(this._routeParams);

  void ngOnInit() {
    player = _routeParams.get('id');
  }

}

Here we’ve anticipated we will retrieve the specific player we are interested in from the route parameters. We use Angular’s RouteParams service to obtain the identifier, and then set a property accordingly. Our detail component doesn’t actually provide much useful detail, of course, but this is just an illustration.

Again, we need to provide a route to the new PlayerDetailComponent by adding the following to AppComponent’s RouteConfig:

  const Route(path: '/player/:id', name: 'PlayerDetail', component: PlayerDetailComponent)

As we planned, this route expects that a player identifier is provided. So let’s update PlayerComponent to display a list of of players. First we include a property containing a simple list of player names

List<String> players = ["John, Tommy", "Carey, Max", "Nehf, Art", "Brown, Mordecai"];

Note: if you now what these ball players have in common, let me know; I’ll be very impressed.

Then update the corresponding template to display this list, registering a click handler for each player shown.

    template: '''
    <h2>Players</h2>
    <ul>
      <li *ngFor="let player of players" (click)="onSelect(player)"></li>
    </ul>''',

Finally, we implement the handler on PlayerComponent to navigate to the new detail route, using the clicked player name as our identifier parameter.

  void onSelect(String player) {
    _router.navigate(['/PlayerDetail', {'id': player}]);
  }

If we run the app now, our players page show a list of player names. Tapping any will show a placeholder detail page for that player.

We now have an app that provides some simple routing. We’d really like if the document titles updated when we routed to a new component!

NB! We’ve cut a lot of corners with our app to keep our example simple!

Injecting the Title Service

The Angular Title service provides a simple API for getting/setting the document title. To utilize the Title service, we’ll need to inject it into any components that require it. Usually a service intended to be used commonly throughout the application is best registered in the parent AppComponent, e.g.

import 'package:angular2/platform/browser.dart' show Title;
[...]
const Provider(Title, useClass: Title) // not the best approach in this case

However, the import of platform/browser implies that AppComponent is aware it is running in the browser. But if we knew that already, we may as well have set the document title directly via the DOM! We want our individual components to be platform-agnostic, so it’s better if we push the registration of the Title service into the Angular bootstrap in main.dart.

In main.dart, we modify our simple bootstrap to additionally register our Title provider, which will be made available throughout our application:

  bootstrap(AppComponent, [
    provide(Title, useFactory: () => new Title(), deps: const[])
  ]);

Here we’ve registered the Title token with a factory constructor that simply instantiates a new Title object.

We can now inject this service into all of our components by altering our component constructors to require it. Here’s an updated FieldsComponent that accepts the injected Title service and assigns it to a field.

import 'package:angular2/core.dart';
import 'package:angular2/router.dart';
import 'package:angular2/platform/browser.dart' show Title;

@Component(
    selector: 'my-fields',
    template: '<h2>Fields</h2>',
)
class FieldsComponent {
  final Title _titleService;

  FieldsComponent(this._titleService);

}

The remaining components follow the same pattern.

//FIXME: we still have to import platform/browser here, doesn’t this violate our requirement that we are platform agnostic?

Updating Titles on Route Changes

Now that we’ve made our Title service available to our components, updating the title is straightforward. We’d like our three components FieldsComponent, PlayersComponent, and TeamsComponent to update the document title whenever they are activated from a route event. Here’s the updated FieldsComponent doing exactly that:

class FieldsComponent implements OnActivate {
  final Title _titleService;
  final RouteParams _routeParams;

  FieldsComponent(this._titleService, this._routeParams);

  @override
  void routerOnActivate(ComponentInstruction nextInst, ComponentInstruction prevInst) {
    _titleService.setTitle("List of Fields");
  }

We’ve implemented the OnActivate interface and its abstract method routerOnActivate. This method simply uses the Title field to set the document title to “List of Fields”. PlayersComponent and TeamsComponent are similarly updated.

Running our app now will show an updated document title when we navigate among the three links. But we still need to address the detail component.

We could modify the PlayerDetailComponent in the same manner above with a new routerOnActivate. But we can also add it to the existing onInit that we use to capture our route parameter:

  void ngOnInit() {
    player = _routeParams.get('id');
    _titleService.setTitle("Player Detail: $player");
  }

And we’re done. The page title will now update to include our player name when we click a player.

Source

The full source code of this example is available on Github.