Angular7入门辅助教程(二)——组件和模板

如果有任何的非技术障碍,比如如何新建Angular项目,请先到我的"Angular7入门辅助教程"专栏参考这篇博客:Angular7入门辅助教程——前篇

Component

Angular中的组件是非常重要的知识点,因为它构成了Angular应用,所以这篇博客来介绍一下Angular中与组件和模板有关的知识点!例如视图、数据绑定、数据显示、组件类与模板的交互等等

心法篇

  • 组件和模板共同构成视图
  • 模板类似于HTML代码段,但是两者是不同的东西,区别之一就是模板可以使用Angular的数据绑定
  • Angular应用由大大小小的组件构成,这些组件要么构成一颗单根组件树(NgModule元数据对象的bootstrap数组只有一个组件)或者森林(NgModule元数据对象的bootstrap数组包含多个组件),不同的组件可以来自同一个NgModule,也可以来自不同的NgModule,这些组件要么通过声明的方式使用(也就是别的组件模板中包含该组件),要么通过路由
  • 我们并不需要显式的创建组件实例,Angular会帮我们做(这一点不懂没关系,到依赖注入之服务服务提供商单例服务这三章或许更好懂些)

详细教程篇

1、组件的元数据

和NgModule类似,组件也需要标识,这个标识就是@Component()装饰器,它接受一个对象作为参数,这个参数就是组件的元数据

@Component({
  selector:    'app-hero-list',
  templateUrl: './hero-list.component.html',
  providers:  [ HeroService ]
})
export class HeroListComponent implements OnInit {
/* . . . */
}

@Component()装饰器接收一个对象作为参数,这个对象就是组件的元数据,而紧跟在@Component()装饰器后的那个类就是组件类,接下来。我们聚焦于这个元数据对象

  • selector(css选择器)——它告诉Angular应该在什么地方插入该组件的实例,例如上面这个组件的css选择器叫app-hero-list,则如果其他的模板中出现了<app-hero-list></app-hero-list>标签,这Angular就会在此插入该组件的一个实例
  • templateUrl(组件模板的url)——说白了,就是模板文件路径,当然,你也可以使用组件元数据对象的template属性来为组件指定一个内嵌的模板
  • providers(服务提供商列表)——这里列出来组件级别的服务提供商,也就是该服务提供商提供的服务实例只能在该组件以及该组件的子组件中使用

2、组件的创建

我们可以使用Angular CLI 使用如下命令快速组件

ng generate component 组件名

例如,我要创建一个名叫hero的组件

ng generate component hero

 这是,Angular CLI会在src/app目录下面生成一个名叫hero的目录,里面有4个文件:hero.component.css(该组件的私有样式文件),hero.component.html(该组件的外联模板文件,通过组件元数据对象的templateUrl属性来引用),hero.component.spec.ts(该组件的测试文件),hero.component.ts,并将该组件的组件类添加到AppModule的declarations数组中

hero.component.ts的内容如下

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-hero',
  templateUrl: './hero.component.html',
  styleUrls: ['./hero.component.css']
})
export class HeroComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}

如果你想在某文件下新建组件,这可以使用下面的命令

ng generate component 文件目录/组件名

3、数据显示

在这里讲解几种在组件模板中显示数据的方式

  • 模板表达式

模板表达式这应该是一种最基础的显示数据的方式了吧! 基本语法是:{{表达式}},双花括号中的表达式可以是数值计算,比如1+1,,也可以是该组件对应的组件类中的属性,例如利用Angular CLI创建新项目时,appComponent的模板文件就绑定了组件类中的title属性,就像下面这样

app.component.html

app.component.ts文件

  • NgFor指令迭代显示数组条目

Angular的这个内置结构型指令,允许你重复创建DOM元素。看下面的例子

在app.component.ts中AppComponent类中加上下面这句话

data: string[] = ['aaaa', 'bbbb', 'ccccc'];

我们要迭代显示这数组中的条目,

修改app.component.html文件如下

<h1>{{ title }}</h1>
<ul>
  <li *ngFor="let item of data">{{ item }}</li>
</ul>

在这里,我们建立了一个名叫item的模板输入变量,它会在data这个数组上面迭代,然后我们使用模板表达式{{item}}来显示数组中的条目 

启动应用

ng serve -o

这时界面如下

  • NgIf指令移除或者创建DOM树

我们可以使用这个结构型指令来整体移除或创建一个DOM树,注意,这里是移除DOM树,而不是隐藏,如果是移除DOM树,Angular就会释放这部分DOM树占据的那部分资源,例如Angular不再检查这部分DOM树的数据变更,这样做的好处是,特别是这课DOM树比较大的时候,会带来性能上的提升,例子如下

我们在app.component.ts的AppComponent类中添加下面一条语句

display = true;

修改app.component.html代码如下

<h1>{{ title }}</h1>
<div *ngIf="display">display</div>
<div *ngIf="!display">not display</div>

启动应用,这是你的界面应该如下

而如果你把display的值改成false,界面就会显示not display

  • NgSwitch指令选择性创建DOM树

该指令实际上是包括三个相互协作的指令:NgSwitch、NgSwitchCase、NgSwitchDefault,其中NgSwitch是属性型指令,后两个是结构型指令

下面来看一个具体的例子

我们在app.component.ts的AppComponent类中添加下面一条语句

heroName = '';
  change(name: string) {
    this.heroName = name;
  }

修改app.component.html代码如下

<h1>{{ title }}</h1>
<button (click)="change('aa')">aa</button>
<button (click)="change('bb')">bb</button>
<button (click)="change('cc')">cc</button>
<div [ngSwitch]="heroName">
  <div *ngSwitchCase="'aa'">aa</div>
  <div *ngSwitchCase="'bb'">bb</div>
  <div *ngSwitchCase="'cc'">cc</div>
  <div *ngSwitchDefault>default name</div>
</div>

启动应用,点击按钮就会在下方显示相应的名字(注意,不要忘记单引号!至于为什么这里的单引号是必须的,请参考本章第七小节:属性绑定和HTML attribute赋值)

4、数据绑定

这个是一个重点知识,数据绑定的目标是DOM中的某些东西,这个目标可能是(元素|组件|指令)的property、(元素|组件|指令)的事件,或(极少数情况下)attribute名,在这里只做简单的介绍,详细请看官方文档:数据绑定

注意:这里同时出现了propertyattribute,他们翻译成中文都是“属性”,但是他们是不一样的东西,Angular的数据绑定一般情况下只会跟property打交道,在这里引用官方的一句话来总结一下它们之间的区别——一般情况下,html元素的attribute和DOM中的property是一一对应的,但是也有例外的!attribute的任务是初始化对应DOM的property的值!

例如,你应该看到过这样的html代码:

<img src="./image/image.jpg"></img>

这里的看到的src就是html的attribute,在DOM树中,有一个相对应的名叫src的DOM property,所以这段代码的意思是设置DOM 的src property的初始值为./image/image.jpg,详细教程请看官方文档:HTML attribute 与 DOM property 的对比 HTML attribute vs. DOM property

下面我们开始Angular的三种类型的数据绑定(对应三种不同的数据流向)

  • 属性绑定

这种类型的绑定的语法:[属性名]="expression",这表示将DOM 对应的property设置为相应的值,这也是为什么这种类型的绑定叫属性绑定,这种绑定的数据流向为数据源(应用)流向视图,对应到这个例子就是:如果这个expression表示的是一个单词的话,那么在这个模板中或者该模板对应的组件类中应该有一个名叫expression的变量!这样,这个变量的值就流向了模板(视图)中

下面来看一个具体的例子 

在app.component.ts的AppComponent类中加入下面这段代码

disabledButton= true;

修改app.component.html代码如下

<h1>{{ title }}</h1>
<button [disabled]="disabledButton">Click</button>

运行应用,发现按钮不可用,如果将disabledButton该成false,发现按钮又可用了

  • 事件绑定

这种类型的绑定语法:(事件名)=“expression”,这表示相应的事件如果发生,这执行expression,所以通常expression是对应的组件类中的一个函数,这种绑定的数据流向:从视图流向数据源(应用),可用这样理解:在执行expression的时候,可能会传入视图中的值(例如input的值)用来设置对应组件类中的某些变量的值!这样,数据就从视图流向了应用

下面来看一个具体的例子

在app.component.ts的AppComponent类中加入下面这段代码

display = true;
change() {
    this.display = !this.display;
  }

修改app.component.html代码如下

<h1>{{ title }}</h1>
<div *ngIf="display">display</div>
<div *ngIf="!display">not display</div>
<button (click)="change()">Click</button>

启动应用,现在你点击按钮会交替显示display和not display 

  • 双向绑定

这种类型的绑定语法:[(target)]="expression",可以发现,这种绑定是属性绑定和事件绑定的结合,对应的数据流也是双向的!

下面来看一个具体的例子

在app.component.ts的AppComponent类中加入下面这段代码

 myData = '';

修改app.component.html代码如下

<h1>{{ title }}</h1>
<input [(ngModel)]="myData" />
<p>{{ myData }}</p>

注意这里还需要改一个地方,因为我们在这里使用了ngModel这个Angular内置的指令,而这个指令属于FormsModule这个模块,所以我们要在AppModule中引入这个模块,

app.module.ts中的部分代码如下

import { FormsModule } from '@angular/forms';

imports: [
    BrowserModule,
    FormsModule
  ],

 现在启动应用,在输入框中输入任意值,发现输入框下方会显示相应的值!这说明,input的值流入了应用(myData),应用中的值(myData)又流到了界面

5、模板变量

Angular中的模板变量有两种:模板输入变量模板引用变量

  • 模板输入变量

这种类型的变量你在前面已经见过(讲解NgFor指令的时候,在这一小节中,我们声明了一个名叫item的模板输入变量,并在表达式中引用它,它是以let这个关键字来声明的)

  • 模板引用变量

声明该变量的语法是在元素中使用#+变量名来声明的,它通常用来引用模板中的某个DOM元素,例如

<input #myInput>

在这个例子中,我们声明了一个名叫myInput的模板引用变量,它引用input这个DOM元素,因此,我们可以使用myInput.value来得到输入框的值

<input #myInput>
<!--使用下面语句可以得到输入框的值-->
{{ myInput.value }}

6、输入属性和输出属性

在前面的例子我们可以看到:数据绑定表达式等号右边的变量均来自模板自己的组件类中,例如在讲解NgIf指令的时候,我们的模板文件的内容是这样的

<h1>{{ title }}</h1>
<div *ngIf="display">display</div>
<div *ngIf="!display">not display</div>

这里的display变量的值就来自该模板对应的组件类中的变量

因为,模板和模板对应的组件类本就是一体,它们共同构建视图,因此,在这种情况下,模板可以任意引用组件类中的变量或方法

但是不同组件之间就不能这么干, 因为,在默认情况下,组件的属性对其他组件是不可见的,这也很合理——自己的东西就不应该然别人任意访问。因此为了让其他组件访问自己的属性,我们就需要做一些特殊处理,它们就是输入属性和输出属性

  • 输入属性

语法:在对外公开的属性前面加上@Input() 修饰符,该类型的属性用于属性绑定,你还可以传入字符串参数为输入属性起别名!像这样

@Input('myName') name: string;

你可能会疑惑,为什么这种类型的属性绑定的变量叫输入属性——因为当你使用这个变量的时候,表达式等号右边的值就“输入”到了这个输入属性上(表达式等号左边),请看下面的实例

 1)、新建一个名叫test-teaching-a的Angular工程,并cd进该目录

ng new test-teaching-a

2)、新建一个名叫titles的组件

ng generate component titles

3)、在titles.component.ts中添加下面这句代码

 myTitles: string[] = [];

4)、修改titles.component.html的内容如下

<p>titles works!</p>
<ul>
  <li *ngFor="let title of myTitles">{{ title }}</li>
</ul>

5)、在app.component.ts中添加下面这句代码

titles: string[] = ['myTitle1', 'myTitle2', 'myTitle3'];

6)、修改app.component.html文件如下

<h1>{{ title }}</h1>
<app-titles [myTitles]="titles"></app-titles>

7)、启动应用,如果不出意外,浏览器的后台应该报如下错误

因为在app.component.html中,myTitles属性属于元素<app-titles>,而titles属性属于appComponent,是不同组件之间的属性绑定,我们需要在myTitles前面加上@Input装饰器

现在修改titles.component.ts的代码如下

import { Component, OnInit, Input } from '@angular/core';

@Component({
  selector: 'app-titles',
  templateUrl: './titles.component.html',
  styleUrls: ['./titles.component.css']
})
export class TitlesComponent implements OnInit {
  // 带上@Input装饰器
  @Input() myTitles: string[] = [];

  constructor() { }

  ngOnInit() {
  }

}

 现在,你的界面应该如下

这样,appComponent中的titles属性的值就流入了titlesComponent中的myTitles中

  • 输出变量

语法:在对外公开的事件方法上加上@Output()装饰器,用在事件绑定中,你也可以像输入参数那样传参数来起别名!因为这里涉及到的是对外公开的事件,因此,我们需要在组件上面新建一个事件,我们使用EventEmitter来自定义事件

1)、在输入变量的这个例子的示例代码的基础上,修改titles.component.ts代码如下

import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-titles',
  templateUrl: './titles.component.html',
  styleUrls: ['./titles.component.css']
})
export class TitlesComponent implements OnInit {
  // 带上@Input装饰器
  @Input() myTitles: string[] = [];

  // 新建事件
  @Output() myEvent = new EventEmitter<string>();

  constructor() {}

  ngOnInit() {}

  addTitle(title: string) {
    // 使用emit方法来触发事件
    this.myEvent.emit(title);
  }
}

需要注意一下上面的注释,myEvent事件是用来传递string类型的消息的(从它的泛型参数可以看出),这些消息会保存在名叫$event的事件对象中,当我们调用emit方法来触发事件的时候,我们传入了string类型的title,我们就可以在模板中通过$event来得到这个值

2)、修改 titles.component.html内容如下

<p>titles works!</p>
<button (click)="addTitle('titleXXXXXXX')">AddTitle</button>
<ul>
  <li *ngFor="let title of myTitles">{{ title }}</li>
</ul>

当点击按钮时,就会调用titpeComponent组件中的addTitle方法,从而触发myEvent事件 

3)、在app.component.ts中添加下面代码

addTitle(title: string) {
    this.titles.push(title);
  }

这个方法是作为myEvent事件的处理器,也就是,当myEvent事件发生时,调用该方法 

4)、修改app.component.html代码如下

<h1>{{ title }}</h1>
<app-titles [myTitles]="titles" (myEvent)="addTitle($event)"></app-titles>

5)、启动应用,点击按钮,会出现类似下图界面

7、属性绑定和HTML attribute赋值

在讲解这个知识点之前,我们先来看一个有趣的小例子,我们修改上面输入变量部分的示例代码中的app.component.html的代码如下

<h1>{{ title }}</h1>
<app-titles myTitles="titles"></app-titles>

 现在,你的应用应该报下面的错误

很明显,Angular直接将titles当成了titles字符串,而你的本意是引用appComponent组件类中的titles属性,这是为什么呢?

记住这个结论——只有在属性绑定,事件绑定、双向绑定,模板表达式、结构型指令等等有Angular语法标识的情况下,Angular才会去计算等号右边表达式的值!!其他情况只会把它当成普通的字符串来处理!!(这里是我自己总结的,可能表达不那么准确,你能明白意思就行

现在,你看到NgSwitch那一小节,现在你明白为什么我要这样写了吧,也就是为什么要加单引号

<div *ngSwitchCase="'aa'">aa</div>

因为*ngSwitchCase是Angular的结构型指令,Angular就会将双引号内的内容解析成对应组件类中的属性,如果我们不加单引号,Angular就回去组件类中找名叫aa的属性,当然找不到啦!

问题篇

你怎样区分数据绑定是在组件类和自己的模板上面进行,还是在不同组件上面进行?或者说,你要在什么情况下为组件的属性加上@Input()/@Output装饰器,而什么情况下不用加呢?

更新中。。。

 

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页