接着昨天的来
给我们的英雄们“美容”
重构.app/app.component.scss
.container{
h1{
text-align: center;
color: red;
}
.row{
line-height: 30px;
padding: 10px 0;
label{
text-align: right;
}
}
.controller{
padding:0 360px;
li{
height: 50px;
}
label{
@extend %link;
text-align: center;
background: #A4D3EE;
}
span{
@extend %link;
text-indent: 5px;
background: #c5f2f2;
}
li:hover{
label{
background: #dce2e0;
}
span{
background: #cfeccf;
}
}
}
}
%link{
height: 40px;
line-height: 40px;
}
一: 选择英雄
我们的应用已经有了英雄列表和单个英雄的详情视图。 但列表和单独的英雄之间还没有任何关联。 我们希望用户在列表中选中一个英雄,然后让这个被选中的英雄出现在详情视图中。 这种 UI 布局模式,通常被称为“主从结构”。 在这个例子中,主视图是英雄列表,从视图则是被选中的英雄。
接下来,我们要通过组件中的一个selectedHero
属性来连接主从视图,它被绑定到了点击事件上。
处理点击事件
我们再往<li>
元素上插入一句点击事件的绑定代码:
<li *ngFor="let item of generals" (click)="oSelect(item)">
圆括号标识<li>
元素上的click
事件是绑定的目标。 等号右边的onSelect(item)
表达式调用 AppComponent
的onSelect()
方法,并把模板输入变量item
作为参数传进去。 它是我们前面在ngFor
指令中定义的那个item
变量。
详细绑定参考https://angular.cn/guide/template-syntax#event-binding
添加点击处理器以暴露选中的英雄
我们不再需要AppComponent
的general
属性,因为不需要再显示单个的英雄,我们只需要显示英雄列表。但是用户可以点选一个英雄。 所以我们要把hero
属性替换成 selectgeneral 属性。
./app/app.component.ts
添加selectGeneral:General;
在用户选取一个英雄之前,所有的英雄名字都应该是未选中的。所以我们不希望像General
一样初始化 selectgeneral 变量。
现在,添加一个onSelect
方法,用于将用户点击的英雄赋给selectgeneral
属性。
./app/app.component.ts
oSelect(item:General):void{
this.selectGeneral=item;
}
.app/app.component.html
添加
<div class="hero_view" >
<div class="row" >
<label class="col-lg-5">id: </label>`selectGeneral`.`id` `selectGeneral`.`name` `selectGeneral`.`source`
</div>
<div class="row" >
<label class="col-lg-5">name: </label>
<input class="col-lg-2" [(ngModel)]='selectGeneral.name' placeholder="name">
</div>
<div class="row" >
<label class="col-lg-5">source: </label>
<input class="col-lg-2" [(ngModel)]='selectGeneral.source' placeholder="name">
</div>
</div>
使用 ngIf 隐藏空的详情
当应用加载时,我们会看到一个英雄列表,但还没有任何英雄被选中。 selectedHero
属性是undefined
。 因此,我们会看到浏览器控制台中出现下列错误:
AppComponent.html:6 ERROR TypeError: Cannot read property 'name' of undefined
我们可以把模板中的英雄详情内容区放在一个<div>
中。 然后,添加一个ngIf
内置指令,把ngIf
的值设置为组件的selectedHero
属性。
<div class="hero_view" *ngIf="selectGeneral">-----------》别忘了ngIf
前的星号 (*
)。
当没有选中英雄时,ngIf
指令会从 DOM 中移除表示英雄详情的这段 HTML 。 没有了表示英雄详情的元素,也就不用担心绑定问题。
当用户选取了一个英雄,selectedHero
变成了“已定义的”值,于是ngIf
把英雄详情加回 DOM 中,并计算它所嵌套的各种绑定。
想了解更多*ngIF-->https://angular.cn/guide/template-syntax#ngif-指令
给选中英雄添加样式
我们在下面的详情区看到了选中的英雄,但是我们还是没法在上面的列表区快速定位这位英雄。
在我们前面添加的styles
元数据中,有一个名叫selected
的自定义CSS类。 要想让选中的英雄更加醒目,当用户点击一个英雄名字时,我们要为<li>
添加selected
类。 例如,当用户点击“Magneta”时,它应该使用不一样的醒目的背景色。
往<li>
上添加一个[class.selected]
绑定:
<li *ngFor="let item of generals" (click)="oSelect(item)" [class.selected]="item==selectGeneral">
当表达式(item === selectGeneral
)为true
时,Angular会添加一个CSS类selected
。为false
时则会移除selected
类。
重构样式
./app/app.component.scss
.container{
h1{
text-align: center;
color: red;
}
.row{
line-height: 30px;
padding: 10px 0;
label{
text-align: right;
}
}
.controller{
padding:0 360px;
li{
height: 50px;
margin-bottom:5px;
}
label{
@extend %link;
text-align: center;
background: #A4D3EE;
}
span{
@extend %link;
text-indent: 5px;
}
li:hover{
background: #c5f2f2;
}
}
}
%link{
height: 40px;
line-height: 40px;
}
.hero_view{
.row{
padding:10px 0;
label{
text-align:right;
}
input{
text-indent: 10px;
}
}
}
.selected{
color:white;
background: #babfc1;
}
二:多个组件
AppComponent
负责所有事。 起初,它只显示单个英雄的详情。然后,它变成了主从结构,同时显示英雄列表和一个英雄详情。 现在,我们很快又会有新需求了。 我们不能把这些需求全都放在一个组件中,否则将不可维护。
我们要把它拆分成一些子组件,每个子组件只聚焦在一个特定的任务或工作流上。 最后,AppComponent
将会变成一个简单的壳,用来作为那些子组件的宿主。
a.制作英雄详情组件
在./app文件下新建general文件夹
.app/general/general-detail.component.ts 这个文件中会存放这个新的 GeneralDetailComponent。
文件名和组件名遵循风格指南中的标准方式。
组件的类名应该是大驼峰形式,并且以
Component
结尾。 因此英雄详情组件的类名是GeneralDetailComponent
。组件的文件名应该是小写中线形式,每个单词之间用中线分隔,并且以
.component.ts
结尾。 因此GeneralDetailComponent
类应该放在general-detail.component.ts
文件中。GeneralDetailComponent的代码如下:
import { Component } from '@angular/core';
@Component({
selector: 'gener-detail',
})
export class GeneralDetailComponent {
}
要定义一个组件,我们总是要先导入符号Component
。
@Component
装饰器为组件提供了Angular元数据。 CSS选择器的名字hero-detail
会匹配元素的标签名,用于在父组件的模板中标记出当前组件的位置。 最后,我们会把<hero-detail>
添加到AppComponent
的模板中。
总是export
这个组件类,因为你必然会在别处import
它。
英雄详情的模板
要把英雄详情的视图移入GeneralDetailComponent
,只要把英雄详情的 内容 从AppComponent
模板的底部剪切出来, 粘贴到@Component
元数据的template
属性中就可以了。
GeneralDetailComponent
有一个 general
属性,而不再是selectGeneral
。 所以我们也要在模板中把所有的selectGeneral
替换为general
。 这些完成之后,新的模板是这样的
@Component({
selector: 'gener-detail',
template:`
<div class="hero_view" *ngIf="general">
<div class="row" >
<label class="col-lg-5">id: </label>`general`.`id` `general`.`name` `general`.`source`
</div>
<div class="row" >
<label class="col-lg-5">name: </label>
<input class="col-lg-2" [(ngModel)]='general.name' placeholder="name">
</div>
<div class="row" >
<label class="col-lg-5">source: </label>
<input class="col-lg-2" [(ngModel)]='general.source' placeholder="source">
</div>
</div>
`,
})
添加general
属性
GeneralDetailComponent
模板绑定到了该组件的genreal
属性上。 把这个属性添加到GeneralDetailComponent
类上,就像这样:
general:General;
因为使用了General类所以需要引入
import {General} from "../../bean/General";
general
属性是一个输入属性
.app/app.component.html
<general-detail [general]='selectGeneral'></general-detail>
在等号的左边,是方括号围绕的general
属性,这表示它是属性绑定表达式的目标。 我们要绑定到的目标属性必须是一个输入属性,否则Angular会拒绝绑定,并抛出一个错误。
首先,修改@angular/core
导入语句,使其包含符号Input
。
import { Component,OnInit} from '@angular/core';
然后,通过在eneral
属性前面加上@Input
装饰器,来表明它是一个输入属性。
@Input()general:General;
现在,general
属性是GeneralDetailComponent
类中唯一的东西。
它所做的一切就是通过它的输入属性general
接收一个英雄对象,然后把这个属性绑定到自己的模板中。
下面是完整的GeneralDetailComponent
:
import { Component,Input} from '@angular/core';
import {General} from "../../bean/General";
@Component({
selector: 'general-detail',
template:`
<div class="hero_view" *ngIf="general">
<div class="row" >
<label class="col-lg-5">id: </label>`general`.`id` `general`.`name` `general`.`source`
</div>
<div class="row" >
<label class="col-lg-5">name: </label>
<input class="col-lg-2" [(ngModel)]='general.name' placeholder="name">
</div>
<div class="row" >
<label class="col-lg-5">source: </label>
<input class="col-lg-2" [(ngModel)]='general.source' placeholder="source">
</div>
</div>
`,
styleUrls:["../app.component.scss"]
})
export class GeneralDetailComponent {
@Input()general:General;
}
重要:在AppModule
中声明GeneralDetailComponent
每个组件都必须在一个(且只有一个)Angular模块中声明。
,app/app.module.ts
import {GeneralDetailComponent} from './general/general-detail.component';
declarations: [
AppComponent,
GeneralDetailComponent
],
完整的app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
//双向数据绑定依赖的模块
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import {GeneralDetailComponent} from './general/general-detail.component';
@NgModule({
declarations: [
AppComponent,
GeneralDetailComponent
],
imports: [
BrowserModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
有哪些变化?
仍然像以前一样,一旦用户点击了英雄的名字,英雄详情就会显示在英雄列表的下方。 不过现在改用HeroDetailView
来表示英雄详情了。
我们把原来的AppComponent
重构成了两个组件具有一些显著优点,无论是现在还是未来:
通过缩减
AppComponent
的职责,我们简化了它。我们将来可以把
GeneralDetailComponent
改进为功能更丰富的英雄编辑器,而不用动AppComponent
。同样,我们也可以改进
AppComponent
而不用动英雄详情视图。我们可以在未来的其它父组件的模板中复用
GeneralDetailComponent
。