接着[Angular2] Case Study:Tour of Heroes - 对象、列表继续。
接下来,将hero detail重构为一个独立的组件。
在app
文件夹下创建文件hero-detail.component.ts
,创建HeroDetailComponent
组件:
import {Component} from 'angular2/core';
@Component({
selector: 'my-hero-detail',
})
export class HeroDetailComponent {
}
将模板从AppComponent
组件中移到HeroDetailComponent
中:
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>
`,
为组件HeroDetailComponent
添加属性hero
:
hero: Hero;
但是这时会发现Hero
接口是定义在AppComponent
中的,现在AppComponent
和HeroDetailComponent
都依赖Hero
接口,因此将它移到单独的模块中,在app
文件夹下创建hero.ts
:
export interface Hero {
id: number;
name: string;
}
分别在AppComponent
和HeroDetailComponent
中添加引用:
import {Hero} from './hero';
注意到。HeroDetailComponent
的hero
属性是个输入参数,AppComponent
在调用它时将selectedHero
传递给它:
<my-hero-detail [hero]="selectedHero"></my-hero-detail>
将hero
定义为输入参数有多种方法,例如在@Component
中配置inputs: ['hero']
,或者使用@Input()
。
接下来,更新AppComponent
,引用HeroDetailComponent
:
import {HeroDetailComponent} from './hero-detail.component';
修改模板:
template:`
<h1>{{title}}</h1>
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="#hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<my-hero-detail [hero]="selectedHero"></my-hero-detail>
`,
这时看起来一切都已经完成了,但是点击后hero的详细信息并没有显示。因为Angular还不认识新的标签<my-hero-detail>
,所以忽略它了。需要在@Component
中添加指令:
directives: [HeroDetailComponent]
现在能正常运行了!
完整的app.component.ts文件如下:
import {Component} from 'angular2/core';
import {Hero} from './hero';
import {HeroDetailComponent} from './hero-detail.component';
@Component({
selector: 'my-app',
template:`
<h1>{{title}}</h1>
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="#hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<my-hero-detail [hero]="selectedHero"></my-hero-detail>
`,
styles:[`
.selected {
background-color: #CFD8DC !important;
color: white;
}
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 10em;
}
.heroes li {
cursor: pointer;
position: relative;
left: 0;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.heroes li.selected:hover {
background-color: #BBD8DC !important;
color: white;
}
.heroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.heroes .text {
position: relative;
top: -3px;
}
.heroes .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
`],
directives: [HeroDetailComponent]
})
export class AppComponent {
title = 'Tour of Heroes';
heroes = HEROES;
selectedHero: Hero;
onSelect(hero: Hero) { this.selectedHero = hero; }
}
var HEROES: Hero[] = [
{ "id": 11, "name": "Mr. Nice" },
{ "id": 12, "name": "Narco" },
{ "id": 13, "name": "Bombasto" },
{ "id": 14, "name": "Celeritas" },
{ "id": 15, "name": "Magneta" },
{ "id": 16, "name": "RubberMan" },
{ "id": 17, "name": "Dynama" },
{ "id": 18, "name": "Dr IQ" },
{ "id": 19, "name": "Magma" },
{ "id": 20, "name": "Tornado" }
];
完整的hero-detail.component.html文件如下:
import {Component} from 'angular2/core';
import {Hero} from './hero';
import {HeroDetailComponent} from './hero-detail.component';
@Component({
selector: 'my-app',
template:`
<h1>{{title}}</h1>
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="#hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<my-hero-detail [hero]="selectedHero"></my-hero-detail>
`,
styles:[`
.selected {
background-color: #CFD8DC !important;
color: white;
}
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 10em;
}
.heroes li {
cursor: pointer;
position: relative;
left: 0;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.heroes li.selected:hover {
background-color: #BBD8DC !important;
color: white;
}
.heroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.heroes .text {
position: relative;
top: -3px;
}
.heroes .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
`],
directives: [HeroDetailComponent]
})
export class AppComponent {
title = 'Tour of Heroes';
heroes = HEROES;
selectedHero: Hero;
onSelect(hero: Hero) { this.selectedHero = hero; }
}
var HEROES: Hero[] = [
{ "id": 11, "name": "Mr. Nice" },
{ "id": 12, "name": "Narco" },
{ "id": 13, "name": "Bombasto" },
{ "id": 14, "name": "Celeritas" },
{ "id": 15, "name": "Magneta" },
{ "id": 16, "name": "RubberMan" },
{ "id": 17, "name": "Dynama" },
{ "id": 18, "name": "Dr IQ" },
{ "id": 19, "name": "Magma" },
{ "id": 20, "name": "Tornado" }
];
hero.ts文件如下:
export interface Hero {
id: number;
name: string;
}
接下来将数据的处理重构到单独的服务模块中。
在app
文件夹中创建文件hero.service.ts
:
import {Injectable} from 'angular2/core';
@Injectable()
export class HeroService {
}
服务的定义中使用了@Injectable()
,它可以让Angular根据元数据实现依赖注入。虽然目前的这个服务不依赖其他模块,但始终使用@Injectable()
是个很好的习惯。
添加getHeroes
方法存根:
@Injectable()
export class HeroService {
getHeroes() {
}
}
之前在AppComponent
中模拟了heroes数组,但是这个数据不应该出现在那里,将它放到一个单独的文件中,在app
文件夹下创建文件mock-heroes.ts
:
import {Hero} from './hero';
export var HEROES: Hero[] = [
{"id": 11, "name": "Mr. Nice"},
{"id": 12, "name": "Narco"},
{"id": 13, "name": "Bombasto"},
{"id": 14, "name": "Celeritas"},
{"id": 15, "name": "Magneta"},
{"id": 16, "name": "RubberMan"},
{"id": 17, "name": "Dynama"},
{"id": 18, "name": "Dr IQ"},
{"id": 19, "name": "Magma"},
{"id": 20, "name": "Tornado"}
];
将app.component.ts
中的HEROES数组移除,并且将heroes
属性改为:
heroes: Hero[];
修改HeroService
,从mock-heroes
中获取数据:
import {HEROES} from './mock-heroes';
import {Injectable} from 'angular2/core';
@Injectable()
export class HeroService {
getHeroes() {
return HEROES;
}
}
在AppComponent
中使用HeroService
,首先添加引用:
import {HeroService} from './hero.service';
那么,现在如何初始化HeroService
呢?建议构造函数依赖注入:
constructor(private _heroService: HeroService) { }
这个构造函数什么都没做,只是定义了一个HeroService
类型的属性_heroService
。
同时还要注册provider,在AppComponent
的@Copmonent
中添加配置:
providers: [HeroService]
如果子组件HeroDetailComponent
需要使用AppComponent
的HeroService
,直接在构造函数里注入就行了,不再需要注册provider。也就是说子组件不要重复父组件的providers。
在AppComponent
中添加getHeroes
方法:
getHeroes() {
this.heroes = this._heroService.getHeroes();
}
那么AppComponent
在哪里调用getHeroes
方法呢?构造函数中?更好的建议是使用lifecycle hook,以保证构造函数中的逻辑尽量少。
OnInit
接口的使用方式为:
```javascript
import {OnInit} from 'angular2/core';
export class AppComponent implements OnInit {
ngOnInit() {
this.getHeroes();
}
}
```
目前HeroService
的getHeroes
是同步的,直接从模拟的数据中读取并立即返回结果。接下来使用promise将它改成异步方式。
修改HeroService
中的getHeroes
方法:
getHeroes() {
return Promise.resolve(HEROES);
}
修改AppComponent
中的getHeroes
方法:
getHeroes() {
this._heroService.getHeroes().then(heroes => this.heroes = heroes);
}
至此,服务的改造就完成了。
完整的hero.service.ts文件如下:
import {Hero} from './hero';
import {HEROES} from './mock-heroes';
import {Injectable} from 'angular2/core';
@Injectable()
export class HeroService {
getHeroes() {
return Promise.resolve(HEROES);
}
// See the "Take it slow" appendix
getHeroesSlowly() {
return new Promise<Hero[]>(resolve =>
setTimeout(()=>resolve(HEROES), 2000) // 2 seconds
);
}
}
完整的app.component.ts如下:
import {Component, OnInit} from 'angular2/core';
import {Hero} from './hero';
import {HeroDetailComponent} from './hero-detail.component';
import {HeroService} from './hero.service';
@Component({
selector: 'my-app',
template:`
<h1>{{title}}</h1>
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="#hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<my-hero-detail [hero]="selectedHero"></my-hero-detail>
`,
styles:[`
.selected {
background-color: #CFD8DC !important;
color: white;
}
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 10em;
}
.heroes li {
cursor: pointer;
position: relative;
left: 0;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.heroes li.selected:hover {
background-color: #BBD8DC !important;
color: white;
}
.heroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.heroes .text {
position: relative;
top: -3px;
}
.heroes .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
`],
directives: [HeroDetailComponent],
providers: [HeroService]
})
export class AppComponent implements OnInit {
title = 'Tour of Heroes';
heroes: Hero[];
selectedHero: Hero;
constructor(private _heroService: HeroService) { }
getHeroes() {
this._heroService.getHeroes().then(heroes => this.heroes = heroes);
}
ngOnInit() {
this.getHeroes();
}
onSelect(hero: Hero) { this.selectedHero = hero; }
}
mock-heroes.ts如下:
import {Hero} from './hero';
export var HEROES: Hero[] = [
{"id": 11, "name": "Mr. Nice"},
{"id": 12, "name": "Narco"},
{"id": 13, "name": "Bombasto"},
{"id": 14, "name": "Celeritas"},
{"id": 15, "name": "Magneta"},
{"id": 16, "name": "RubberMan"},
{"id": 17, "name": "Dynama"},
{"id": 18, "name": "Dr IQ"},
{"id": 19, "name": "Magma"},
{"id": 20, "name": "Tornado"}
];
PS:在HeroServise
中添加getHeroesSlowly
方法模拟缓慢的处理:
getHeroesSlowly() {
return new Promise<Hero[]>(resolve =>
setTimeout(()=>resolve(HEROES), 2000) // 2 seconds
);
}
参考资料
Angular2官方文档