接着[Angular2] Case Study:Tour of Heroes - 多组件、服务继续,添加一个视图,实现在多个视图之间的路由。
我们的目标是:
1.将AppComponent
改成只处理页面导航
2.将AppComponent
中的关于heroes的逻辑移到HeroesComponent
组件中
3.添加路由
4.添加DashboardComponent
组件
5.将Dashboard
添加到导航结构中
直接将AppComponent
重命名为HeroesComponent
,再创建一个新的AppComponent
:
app.component.ts
文件重命名为heroes.component.ts
;AppComponent
重命名为HeroesComponent
;my-app
改成my-heroes
。新的AppComponent
相当于应用的shell,它的顶部有几个导航链接,下面是一块显示区域,用于显示导航到的页面。
import { Component } from 'angular2/core';
import { HeroService } from './hero.service';
import { HeroesComponent } from './heroes.component';
@Component({
selector: 'my-app',
template: `
<h1>{{title}}</h1>
<my-heroes></my-heroes>
`,
directives: [HeroesComponent],
providers: [
HeroService
]
})
export class AppComponent {
title = 'Tour of Heroes';
}
注意,这里的providers
数组中包含了HeroService
,因此需要将HeroesComponent
的providers
中的HeroService
去掉。
在index.html
中添加路由模块:
<script src="node_modules/angular2/bundles/router.dev.js"></script>
同时,在index.html
的<head>
内的顶部添加<base href="/">
:
<head>
<base href="/">
路由组件是一个服务,因此需要引入它,并且将它添加到providers
数组中。在app.component.ts
中添加:
...
import { RouteConfig, ROUTER_DIRECTIVES, ROUTER_PROVIDERS } from 'angular2/router';
...
directives: [ROUTER_DIRECTIVES],
providers: [
ROUTER_PROVIDERS,
HeroService
]
...
使用@RouteConfig
装饰器来为AppComponent
添加路由并配置:
@RouteConfig([
{
path: '/heroes',
name: 'Heroes',
component: HeroesComponent
}
])
@RouteConfig
接收测参数是路由定义数组。一个路由定义包含三部分:
path
:该路由匹配的URL路径。name
:路由的名称。为了避免和path
混淆,路由名称的首字母必须大写。component
:导航到该路由时需要创建的组件。如果在浏览器中访问/heroes
,这个路径是与路由Heroes
匹配的,应该显示HeroesComponent
组件,但是在哪里显示呢?
在模板的下方添加<router-outlet>
标记。RouterOutlet
是一个ROUTER_DIRECTIVES
指令。路由会将导航到的组件显示在<router-outlet>
的下方。
添加一个链接标签,点击它触发导航到相应的组件:
// app.component.js
template: `
<h1>{{title}}</h1>
<a [routerLink]="['Heroes']">Heroes</a>
<router-outlet></router-outlet>
`,
RouterLink
也是一个ROUTER_DIRECTIVES
指令。
现在,在浏览器中可以看到,页面打开是不显示heroes列表了,点击Heroes
链接后才显示。
到这里,完整的app.component.ts文件如下:
import { Component } from 'angular2/core';
import { RouteConfig, ROUTER_DIRECTIVES, ROUTER_PROVIDERS } from 'angular2/router';
import { HeroService } from './hero.service';
import { HeroesComponent } from './heroes.component';
@Component({
selector: 'my-app',
template: `
<h1>{{title}}</h1>
<a [routerLink]="['Heroes']">Heroes</a>
<router-outlet></router-outlet>
`,
directives: [ROUTER_DIRECTIVES],
providers: [
ROUTER_PROVIDERS,
HeroService
]
})
@RouteConfig([
{
path: '/heroes',
name: 'Heroes',
component: HeroesComponent
}
])
export class AppComponent {
title = 'Tour of Heroes';
}
路由只有在有多个视图时才有意义。添加一个新的视图:
import { Component } from 'angular2/core';
@Component({
selector: 'my-dashboard',
template: '<h3>My Dashboard</h3>'
})
export class DashboardComponent { }
在AppComponent
中添加路由配置:
{
path: '/dashboard',
name: 'Dashboard',
component: DashboardComponent,
useAsDefault: true
},
PS:useAsDefault
属性之指定为true
,那么页面导航到/
后会默认导航到该路由。
在模板中添加Dashboard
的导航链接,:
template: `
<h1>{{title}}</h1>
<nav>
<a [routerLink]="['Dashboard']">Dashboard</a>
<a [routerLink]="['Heroes']">Heroes</a>
</nav>
<router-outlet></router-outlet>
`,
PS:这里还添加了<nav>
标签,方便后面添加样式。
在Dashboard中显示最前面的四个hero。这里我们将模板放到单独的文件中,创建文件ashboard.component.html
:
<h3>Top Heroes</h3>
<div class="grid grid-pad">
<div *ngFor="#hero of heroes" (click)="gotoDetail(hero)" class="col-1-4" >
<div class="module hero">
<h4>{{hero.name}}</h4>
</div>
</div>
</div>
将元数据中的template
改成templateUrl
指向dashboard.component.html
:
templateUrl:'app/dashboard.component.html'
这时,gotoDetail
方法和heroes
属性都还没有定义,接下来完善它。
之前,我们将HeroService
从HeroesComponent
的providers
中移到了AppComponent
中,这样应用会创建一个单例的HeroService
,对所有组件都是可用的,在DashboardComponent
中直接注入就可以了。
修改DashboardComponent
,引入所需的组件:
import { Component, OnInit } from 'angular2/core';
import { Hero } from './hero';
import { HeroService } from './hero.service';
添加的实现:
export class DashboardComponent implements OnInit {
heroes: Hero[] = [];
constructor(private _heroService: HeroService) { }
ngOnInit() {
this._heroService.getHeroes()
.then(heroes => this.heroes = heroes.slice(1,5));
}
gotoDetail(){ /* not implemented yet */}
}
接下来,添加一个路由,导航到HeroDetailComponent
来显示Hero Detail。继续在AppComponent
中添加路由配置。
但是,这里有点不同,我们必须告诉HeroDetailComponent
显示哪一个hero。
可以在URL中指定hero的id。例如/detail/11
。路由定义为:
{
path: '/detail/:id',
name: 'HeroDetail',
component: HeroDetailComponent
},
注意这时AppComponent
中要添加HeroDetailComponent
的引用:
import { HeroDetailComponent } from './hero-detail.component';
目前HeroDetailComponent
是这样的:
import {Component} from 'angular2/core';
import {Hero} from './hero';
@Component({
selector: 'my-hero-detail',
template: `
<div *ngIf="hero">
<h2>{{hero.name}} details</h2>
<div>
<label>id: </label>{{hero.id}}
</div>
<div>
<label>name: </label>
<input [(ngModel)]="hero.name" placeholder="name"/>
</div>
</div>
`,
inputs: ['hero']
})
export class HeroDetailComponent {
hero: Hero;
}
现在不再需要从父组件绑定hero属性来接收参数了,新的HeroDetailComponent
应该从路由中获取参数id,然后使用HeroService
来获取数据。
添加相关的引用:
import {RouteParams} from 'angular2/router';
import { HeroService } from './hero.service';
import { Component, OnInit } from 'angular2/core';
在构造函数中注入RouteParams
和HeroService
:
constructor(
private _heroService: HeroService,
private _routeParams: RouteParams) {
}
实现ngOnInit
,从RouteParams
中获取参数id的值,然后使用HeroService
来获取该id的hero:
ngOnInit() {
let id = +this._routeParams.get('id');
this._heroService.getHero(id)
.then(hero => this.hero = hero);
}
使用RouteParams.get
方法来获取路由中的参数。路由参数始终是字符串。这里使用+
将它转换成了数字。
在HeroService
中添加getHero
方法:
javascript getHero(id: number) { return Promise.resolve(HEROES).then( heroes => heroes.filter(hero => hero.id === id)[0] ); }
当导航到HeroDetailComponent
后,如何再导航到其它页面呢?
用户可以点击AppComponent
的导航链接,或者使用浏览器的回退按钮。还可以在HeroDetailComponent
中添加一个回退按钮,顺便将模板移到单独的文件hero-detail.component.html
中:
<div *ngIf="hero">
<h2>{{hero.name}} details!</h2>
<div>
<label>id: </label>{{hero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="hero.name" placeholder="name" />
</div>
<button (click)="goBack()">Back</button>
</div>
在HeroDetailComponent
中添加goBack
方法:
goBack() {
window.history.back();
}
现在完善一下DashboardComponent
中的gotoDetail
方法:
gotoDetail(hero: Hero) {
let link = ['HeroDetail', { id: hero.id }];
this._router.navigate(link);
}
为DashboardComponent
添加路由:
import { Router } from 'angular2/router';
constructor(
private _router: Router,
private _heroService: HeroService) {
}
现在在浏览器中点击dashboard中的hero,就会跳转到hero detail页面,再点击Back就能回到前一个页面。
接下来重构HeroesComponent
,去掉<my-hero-detail>
标签,替换为:
<div *ngIf="selectedHero">
<h2>
{{selectedHero.name | uppercase}} is my hero
</h2>
<button (click)="gotoDetail()">View Details</button>
</div>
PS:这里使用了UpperCasePipe
,将字符串处理成字母全部大写。
这时,整个heroes.component.ts
文件内容显得有点太长了,将模板和样式分别移到单独的文件heroes.component.html
和heroes.component.css
中,在元数据中使用templateUrl
和styleUrls
来分别引用它们。然后,引用router并在构造函数中注入,实现gotoDetail
方法。最后heroes.component.ts
为:
import {Component, OnInit} from 'angular2/core';
import { Router } from 'angular2/router';
import {Hero} from './hero';
import {HeroDetailComponent} from './hero-detail.component';
import {HeroService} from './hero.service';
@Component({
selector: 'my-heroes',
templateUrl: 'app/heroes.component.html',
styleUrls: ['app/heroes.component.css'],
directives: [HeroDetailComponent]
})
export class HeroesComponent implements OnInit {
heroes: Hero[];
selectedHero: Hero;
constructor(
private _router: Router,
private _heroService: HeroService) { }
getHeroes() {
this._heroService.getHeroes().then(heroes => this.heroes = heroes);
}
ngOnInit() {
this.getHeroes();
}
onSelect(hero: Hero) { this.selectedHero = hero; }
gotoDetail() {
this._router.navigate(['HeroDetail', { id: this.selectedHero.id }]);
}
}
在app
文件夹下创建文件hero-detail.component.css
、dashboard.component.css
和app.component.css
,分别在各自组件的元数据中配置styleUrls
:
/* hero-detail.component.css */
label {
display: inline-block;
width: 3em;
margin: .5em 0;
color: #607D8B;
font-weight: bold;
}
input {
height: 2em;
font-size: 1em;
padding-left: .4em;
}
button {
margin-top: 20px;
font-family: Arial;
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer; cursor: hand;
}
button:hover {
background-color: #cfd8dc;
}
button:disabled {
background-color: #eee;
color: #ccc;
cursor: auto;
}
/* dashboard.component.css */
[class*='col-'] {
float: left;
}
*, *:after, *:before {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
h3 {
text-align: center; margin-bottom: 0;
}
[class*='col-'] {
padding-right: 20px;
padding-bottom: 20px;
}
[class*='col-']:last-of-type {
padding-right: 0;
}
.grid {
margin: 0;
}
.col-1-4 {
width: 25%;
}
.module {
padding: 20px;
text-align: center;
color: #eee;
max-height: 120px;
min-width: 120px;
background-color: #607D8B;
border-radius: 2px;
}
h4 {
position: relative;
}
.module:hover {
background-color: #EEE;
cursor: pointer;
color: #607d8b;
}
.grid-pad {
padding: 10px 0;
}
.grid-pad > [class*='col-']:last-of-type {
padding-right: 20px;
}
@media (max-width: 600px) {
.module {
font-size: 10px;
max-height: 75px; }
}
@media (max-width: 1024px) {
.grid {
margin: 0;
}
.module {
min-width: 60px;
}
}
/* app.component.css */
h1 {
font-size: 1.2em;
color: #999;
margin-bottom: 0;
}
h2 {
font-size: 2em;
margin-top: 0;
padding-top: 0;
}
nav a {
padding: 5px 10px;
text-decoration: none;
margin-top: 10px;
display: inline-block;
background-color: #eee;
border-radius: 4px;
}
nav a:visited, a:link {
color: #607D8B;
}
nav a:hover {
color: #039be5;
background-color: #CFD8DC;
}
nav a.router-link-active {
color: #039be5;
}
在项目的根目录下添加创建styles.css
:
h2 {
color: #444;
font-family: Arial, Helvetica, sans-serif;
font-weight: lighter;
}
body {
margin: 2em;
}
body, input[text], button {
color: #888;
font-family: Cambria, Georgia;
}
button {
font-family: Arial;
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
cursor: hand;
}
button:hover {
background-color: #cfd8dc;
}
button:disabled {
background-color: #eee;
color: #aaa;
cursor: auto;
}
/* everywhere else */
* {
font-family: Arial, Helvetica, sans-serif;
}
并在index.html
中添加样式文件的引用:
<link rel="stylesheet" href="styles.css">
最后效果如图:
源码
参考资料
Angular2官方文档