第一章 编写第一个AngularWeb应用
1.1 仿制Reddit网站项目
Reddit是建立在投票机制上的社交新闻平台,任何人都可以发布任何链接,划分不同板块,最多“赞”的链接将上升到页面的顶部。
这简单的应用涵盖Angular大部分基本要素,包括:
- 构建自定义组件
- 从表单中接收用户输入
- 把对象列表渲染到视图中
- 拦截用户的点击操作,并据此作出反应
1.2 准备
1.2.1 TypeScript
TypeScript是JavaScript ES6版的一个超集,增加了类型支持。Angluar(内置ES5 API)并不必须需要TypeScript,使用它是因为它能让Angluar写起来简单。
使用TypeScript要安装Node.js
1.2.1.1 安装node.js
我安装的位置是:E:\SoftWare\Node,而环境变量的Path应有:
测试 node -v和npm -v:
说明node安装成功。
1.2.1.2 安装TypeScript
运行命令(注意重启电脑,使环境变量path有效,并且添加--registry=http://registry.npm.taobao.org使用淘宝的镜像比较快速):
$ npm install -g typescript --registry=http://registry.npm.taobao.org
$ npm config set registry https://registry.npm.taobao.org //加这个后后面的安装命令都会找淘宝的镜像
测试是否安装成功:
$ tsc -v
1.2.1.3 安装angular-cli
Angular提供了一个命令行工具angular-cli,用户通过命令行创建和管理项目。它自动化了一系列任务,比如创建项目、添加新的控制器。
$ npm install -g @angular/cli
测试安装成功:
$ ng
有时需要watchman的工具(我的window不用安装),此工具帮助angular-cli监听系统的变化,如果在OS X上运行,建议使用Homebrew工具安装它。
$ brew install watchman
Mac OS X用户:brew出错,请参阅http://brew.sh/来安装它。
Linux用户:请参阅https://ember-cli.com/user-guide/#watchman来学习安装watchman。
Win用户:不用安装任何东西,angular-cli将使用node.js文件监视器。
1.2.2 创建angular项目
运行:
$ ng new angular2-hello-world
出现Cannot download "https://github.com/sass/node-sass/releases/download/v4.11.0 错误时,请:
①npm install -g cnpm --registry=https://registry.npm.taobao.org
②python下载安装:https://www.python.org/downloads/windows/
③cnpm install node-sass
这里有以下解决这问题的各种方案:https://www.jianshu.com/p/a0641a990206
之后重新运行新建命令。打开src下index.html文件
<body>
<app-root>Loading...</app-root>
</body>
应用会在将在app-root标签处渲染,文本Loading...是一个占位符,在应用代码加载之前显示它。我们借助此技巧来通知用户该应用正在加载,可以像这里一样显示一条消息,也可以显示一个加载动画或其他形式的进度通知。
1.2.3 运行应用
angular-cli应用有内建的HTTP服务器,用它启动应用,进入应用的根目录,我的是D:\angular_pro\angular2-hello-world。
$ ng serve
应用正在localhost的4200端口上运行,打开浏览器并访问http://localhost:4200。
用VSCode打开Angular项目:
选择文件——打开文件夹,选择angluar2-hello-world就行了:
1.3 制作Component
Angluar特点之一就是组件化。比如<select>、<form>和<video>。不过它们功能都是浏览器开发者预定义好的。想让浏览器认识一些新(自定义)标签。比如<weather>标签显示天气,<login>标签,创建一个登录面板。创建一个<app-hello-world>组件。
ng generate component hello-world
组件基本组件包括:
- Component注解
- 注解定义类
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-hello-world',
templateUrl: './hello-world.component.html',
styleUrls: ['./hello-world.component.css']
})
export class HelloWorldComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}
TypeScript文件的后缀是.ts而不是.js。浏览器不会解释ts文件,因此ng server命令会自动把.ts文件编译为.js文件。
1.3.1 导入依赖
improt语句定义了写代码要用到那些模块,导入两样东西:Component和OnInit。我们从"@angular/core"模块中导入了组件。名字分别导出两个JavaScript/TypeScript对象,名字分别是Component和OnInit。
注意这个Import{things} from wherever 格式。{things }叫做解构。解构是由ES6和TypeScript提供的一项特性。import从另一个模块拉取依赖,并让这些依赖在当前文件可用。
1.3.2 Component注解
导入依赖后,我们还要声明该组件。
@Component({
selector: 'app-hello-world',
templateUrl: './hello-world.component.html',
styleUrls: ['./hello-world.component.css']
})
Angular的依赖注入技术在幕后使用了注解的概念。但注解其实是让编译器为代码添加功能的途径之一。注解看作添加到代码上的元数据。当在HelloWorld类使用@Component时,就把HelloWorld装饰成了一个Component。这个<app-hello-world>标签表示我们希望在HTML使用组件。要实现它,配置@Component并把Selector指定app-hello-world。
有很多方式配置选择器,类似CSS选择器,XPath或JQuery选择器。Angular组件对选择器的混用方式添加了一些特有的限制。这里的selector属性用来指出该组件将使用哪个Dom元素。如果模板中有<app-hello-world></app-hello-world>标签,就用Component类及其组件定义信息对其进行编译。
1.3.3 用TemplateUrl添加模板
在这个组件中,把templateUrl指定为./hello-world.component.html。从与组件同目录的hello-word.component.html文件中加载模板。
<p>
hello-world works!
</p>
这里定义了一个p标签,其中包含了一些简单文本。当Angular加载组件,就会读取此文件的内容作为组件的模板。
1.3.4 添加template
定义模板的方式:使用@Component对象中的template属性;指定templateUrl属性。
@Component({
selector:'app-hello-world',
template: '
<p>
hello-world works!
</p>
'
})
我们在反引号中('...')定义了template字符串。这是ES6中的一个新特性,允许使用多行的字符串。使用反引号定义多行字符串,可以让模板放到代码文件中。
1.3.5 用styleUrls添加CSS样式
注意styleUrls属性:
styleUrls:['./hello-world.component.css']
使用hello-world.component.css文件中的css作为组件的样式,angular使用一项叫做样式封装的技术,它意味着在特定组件中指定样式只会应用于该组件本身。与template不同的是它接受一个数组型参数。可以为同一个组件加载多个样式表。
1.3.6 加载组件
打开项目的src/app/app.component.html。
<h1>
Welcome to {{ title }}!
<app-hello-world></app-hello-world>
</h1>
会出现上面情况。
1.4 把数据添加到组件中
上面的组件渲染了一个静态模板。下面创建一个动态的新的组件——显示用户名字。
$ ng generate component use-item
创建好的组件,添加到一个模板中。
<h1>
Welcome to {{ title }}!
<app-hello-world></app-hello-world>
<app-user-item></app-user-item>
</h1>
希望UserItemComponent显示特定用户的名字,引入name并声明为组件的一个新属性。有了name属性,就能在不同的用户之间复用组件了。为了添加名字,要在UserItemComponent类引入一个属性,来声明该组件有一个名叫name的局部变量。在user-item/user-item.component.ts
export class UseItemComponent implements OnInit {
name:string;
constructor() {
this.name="Felipe";
}
ngOnInit() {
}
}
在name:string;中,name是我们想设置的属性名,而string是该属性的类型。为name指定类型是TypeScript中的特性。用来确保它的值必须是string。这些代码在User-ItemComponent类的实例设置了name的属性。并且编译器会确保name是string类型。
构造函数,这些函数在创建这个类的实例时自动调用。在构造函数中,可以通过this.name设置name属性。在构造函数中,通过this.name来设置name属性。
填好这个值后,可以用模板语法在模板中显示该变量的值。
<p>
Hello {{name}}
</p>
{{name}}。括号叫做模板标签。模板标签中间的任何东西都会被当作一个表达式。这里,因为template绑定这个组件,name会展开为this.name的值。
1.5 使用数组
在angular,ngfor是类似指定。作用是一样的:为一组对象反复渲染同样的页面脚本。创建一个新的组件。
$ ng generate component user-list
把<app-user-item>替换成<app-user-list>。
names:string[];
constructor() {
this.names=["Ari","Carlos","Felipe","Nate"];
}
string[]表示string类型构成的数组,另一种写法是Array<string>。还修改了构造函数,让它this.name=['Ari','Carlos','Felipe','Nate']。更新模板,渲染出这个列表了。这时要用到*ngFor,它会在一个列表上迭代,为列表中每一个条目生成一个新标签。
<p>
<li *ngFor="let name of names">Hello {{name}}</li>
</p>
*ngFor语法是在这个属性使用NgFor指令,把NgFor理解一个类似for的循环,其目的为集合的每个条目都新建一个Dom元素。let name of names 循环处理names中的每个元素并将其逐个赋值给一个名叫name的局部变量。
NgFor指令为数组names中的每一个条目都渲染一个li标签,并声明一个本地变量name来持有当前迭代的条目,这个新的变量将插值到Hello{{name}} 代码片段里。
1.6 使用UserItemComponent组件
这次不会再UserListComponent中直接渲染每个名字了,而是改用UserItemComponent作为子组件。我们不再直接重复渲染li标签,而是让UserItemComponent来为列表中的每个条目指定模板。
- 配置UserListComponent来渲染UserItemComponent。
- 配置UserItemComponent来接收name变量作为输入。
- 配置UserListComponent的模板来把用户名传给UserItemComponent。
1.6. 1 渲染UserItemComponent
UserItemComponent制定了选择器app-user-item,接下来把这个标签添加到模板中,把li标签替换为app-user-item标签。
<ul>
<app-user-item
*ngFor="let name of names"></app-user-item>
</ul>
Angular提供了一种方式:@Input注解。
1.6. 2 接收输入
UserItemComponent已经再其构造函数中设置了this.name='Felipe'。让组件的name属性从外部接收值。
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'app-user-item',
templateUrl: './user-item.component.html',
styleUrls: ['./user-item.component.css']
})
export class UserItemComponent implements OnInit {
@Input() name:string;
constructor() {
}
ngOnInit() {
}
}
1.6. 3 传入Input值
带方括号的属性,意味着把一个值传给该组件同名的输入属性。
<ul>
<app-user-item
*ngFor="let individualUserName of names" [name]="individualUserName"></app-user-item>
</ul>
1.6. 4 ng serve
angular-cli.json文件会找到该应用的入口,指定一个"main"文件,引导bootstrap应用,引导会创建angular模块,使用AppModule引导该应用,它是在src/app/app.module.ts中指定的。AppModule指定了哪个组件用作顶层组件。这里是AppComponent,AppComponent的模板中有一个<app-user-list>标签,渲染出我们的用户列表。
app.module.ts下的@NgModule注解有三个属性:declarations、imports和bootstrap。
declarations指定了在该模板中定义的组件
imports描述了该模块有哪些依赖。
bootstrap当使用该模块引导应用时,要把AppComponent加载为顶层组件。
1.7 仿造Reddit
1.7.1 新建angular2-reddit
在D:\angular_pro\新建一个Reddit项目。并且用vscode打开。
ng new angular2-reddit
1.7.2 添加CSS
从 https://github.com/haiiiiiyun/ng-book2-r51-code 的 first_app/angular2_reddit 目录下复制以下文件到相应目录下。
复制以下文件到应用目录下:
- src/index.html
- src/style.css
- src/app/vendor
- src/assets/images
1.7.3 应用程序组件
构建新的组件,用来:
- 存储当前文章列表
- 包含一个表单,用来提交新的文章
打开src/app/app.component.ts文件中找到主应用组件。对template(也就是src/app/app.component.html)修改,定义了两个input标签:一个用于文章的标题(title),另一个用于文章的(link URL)。
<form class="ui large form segment">
<h3 class="ui header">Add a Link</h3>
<div class="field">
<label for="title">Title:</label>
<input name="title">
</div>
<div class="field">
<label for="link">LiNk:</label>
<input name="link">
</div>
</form>
1.7.4 添加互动
addArticle(title:HTMLInputElement,link:HTMLInputElement):boolean{
console.log(`Adding article title:${title.value} and link:${link.value}`);//注意是反引号
return false;
}
在app.component.html上:
<form class="ui large form segment">
<h3 class="ui header">Add a Link</h3>
<div class="field">
<label for="title">Title:</label>
<input name="title" #newtitle>
</div>
<div class="field">
<label for="link">LiNk:</label>
<input name="link" #newlink>
</div>
<button (click)='addArticle(newtitle,newlink)' class="ui positive right floated button">
Submit Link
</button>
</form>
注意以下几点:
- 输入标签用#newtitle属性获取值,在button上加入点击事件是(click)='addArticle(newtitle,newlink)'。
- js获取参数用(title:HTMLInputElement,link:HTMLInputElement),输出参数值是`${title.value}`,注意是反引号(tab键上面一键),不是单引号。
- addArticle是组建定义类AppComponent的一个函数,newtitle来自名叫title的<input>标签的解析。注意title和link是HTMLInputElement类型的对象,而非直接输入的值
1.7.5 添加文章组件
显示文章列表,用ng工具生成新组件:
$ ng generate component article
<div class="four wide column center aligned votes">
<div class="ui startistic">
<div class="value">
{{votes}}
</div>
<div class="label">
Points
</div>
</div>
</div>
<div class="twelve wide column">
<a class="ui large header" href="{{link}}">
{{title}}
</a>
<ul class="ui big horizontal list voters">
<li class="item">
<a href (click)="voteUp()">
<i class="arrow up icon"></i>
upvote
</a>
</li>
<li class="item">
<a href (click)="voteDown()">
<i class="arrow down icon"></i>
downvote
</a>
</li>
</ul>
</div>
左侧是投票的数量,右侧是文章的信息。分别用four wide column和twelve wide column这两个CSS类来指定这两列。
- 用模板展开字符串{{votes}}和{{title}}属性,这些值来自ArticleComponent类中的votes和title属性
- 可以在属性值中使用模板字符串,比如在a标签的href属性中:href="{{link}}"。在这种情况下,href的值会跟随组件类的link属性的值进行动态插值(近似)计算
- 在upvote和downvote的链接上。
@Component({
selector: 'app-article',
templateUrl: './article.component.html',
styleUrls: ['./article.component.css'],
host:{
class:'row'
}
})
用@Componenr定义了一个新组件。selector表示会用<app-article>标签将该组件放在页面中。当页面被渲染出来时,这些标签会留在视图中。使用Semantic UI的CSS类的row表示app-article独占一行。在angular中,组件的宿主就是该组件附着的元素。host:{class:'row'}告诉angular:要在宿主元素(app-article标签)上设置class属性,使其具有row类。
export class ArticleComponent implements OnInit {
votes:number;
title:string;
link:string;
constructor() {
this.title='Angular 2';
this.link="http://angular.io";
this.votes=10;
}
voteUp(){
this.votes+=1;
}
voteDown(){
this.votes-=1;
}
ngOnInit() {
}
在app.component.html用这个article标签
<div class="ui grid posts">
<app-article>
</app-article>
</div>
如果刷新不出来,我们需要把Article-Component添加到NgModule的declaration列表中。
(1)用import导入ArticleComponent
(2)把ArtcleComponent添加到declaration列表中。
刷新浏览器,就看到该文章正确渲染出来了。
由于js会把click事件冒泡到所有父级组件中,因为click事件被冒泡了父级,浏览器就会尝试导航这个空白链接,点击会就出现刷新。
现在点击这个链接,会看到投票数正确地增加或减少了,没有出现多余地页面刷新。
1.8 渲染多行
1.8.1 创建Article类
写angular代码时的最佳实践之一就是尝试从组件代码中把你正在使用的数据结构隔离出来。要做这一点,创建一个数据结构,用以表示单个文章。
export class Article{
title:string;
link:string;
votes:number;
constructor(title:string,link:string,votes?:number){
this.title=title;
this.link=link;
this.votes=votes||0;
}
}
此处,创建了一个新类,用来表示Article。这是一个普通类而不是Angular组件。在MVC模型中,它被称为模型。每篇文章都有一个标题title,一个链接link和一个投票总数votes。当创建新文章时,我们需要title和link。votes参数是可选的,并且默认为0。
修改ArticleComponent的代码,使用新的Article类。
import { Component, OnInit } from '@angular/core';
import {Article} from './article.model';
@Component({
selector: 'app-article',
templateUrl: './article.component.html',
styleUrls: ['./article.component.css'],
host:{
class:'row'
}
})
export class ArticleComponent implements OnInit {
article:Article;
constructor() {
this.article=new Article(
'Angular 2',
'http://angular.io',
10
)
}
voteUp(){
this.article.votes+=1;
return false;
}
voteDown(){
this.article.votes-=1;
return false;
}
ngOnInit() {
}
}
修改视图,以前用的是{{votes}},现在要改成{{article.votes}}。
情况好多了,voteUp和voteDown直接修改了文章的内部属性。
1.8.2 存储多篇文章
展示有多个Article的列表,从让AppComponent拥有一份文章集合开始。
app.component.ts
import { Component } from '@angular/core';
import { Article } from './article/article.model';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'app works';
articles:Article[];
constructor(){
this.articles=[
new Article('Angular 2','http://angular.io',3),
new Article('Fullstack','http://fullstack.io',2),
new Article('Angular Homepage','http://angular.io',1)
];
}
addTitle(title:HTMLInputElement,link:HTMLInputElement):boolean{
this.articles.push(new Article(title.value,link.value,0));
title.value='';
link.value='';
return false;
}
}
app.component.html
<!--The content below is only a placeholder and can be replaced.-->
<form class="ui large form segment">
<h3 class="ui header">Add a Link</h3>
<div class="field">
<label for="title">Title:</label>
<input name="title" #newtitle>
</div>
<div class="field">
<label for="link">LiNk:</label>
<input name="link" #newlink>
</div>
<button (click)='addArticle(newtitle,newlink)' class="ui positive right floated button">
Submit Link
</button>
</form>
<div class="ui grid posts">
<app-article *ngFor="let foobar of articles" [article]="foobar">
</app-article>
</div>
article.component.ts
import { Component, OnInit, Input } from '@angular/core';
import {Article} from './article.model';
@Component({
selector: 'app-article',
templateUrl: './article.component.html',
styleUrls: ['./article.component.css'],
host:{
class:'row'
}
})
export class ArticleComponent implements OnInit {
@Input() article:Article;
voteUp():boolean{
this.article.voteUp();
return false;
}
voteDown():boolean{
this.article.voteDown();
return false;
}
ngOnInit() {
}
}
article.component.html
<div class="four wide column center aligned votes">
<div class="ui startistic">
<div class="value">
{{article.votes}}
</div>
<div class="label">
POINTS
</div>
</div>
</div>
<div class="twelve wide column">
<a class="ui large header" href="{{article.link}}">
{{article.title}}
</a>
<ul class="ui big horizontal list voters">
<li class="item">
<a href (click)="voteUp()">
<i class="arrow up icon"></i>
upvote
</a>
</li>
<li class="item">
<a href (click)="voteDown()">
<i class="arrow down icon"></i>
downvote
</a>
</li>
</ul>
</div>
articles:Article[];意思是articles是Article模型的数组。另一种写法是Array<Article>。这种模式被称为泛型。Array是一个集合,只能存放Article类型的对象。
1.8.3 使用Inputs配置ArticleComponent
假设我们有两篇文章article1和article2。要把一个Article型的"参数"传给组件复用app-article组件。
<app-article [article]="article1"></app-article>
<app-article [article]="article2"></app-article>
[]就是输入属性,article就是获取变量。
1.8.4 渲染文章列表
<app-article *ngFor="let foobar of articles" [article]="foobar">
</app-article>
这里:
- articles是一个Article的数组,由AppComponent组件定义
- footbar是一个articles数组中的单个元素,由ngFor定义
- article是一个字段名,由ArticleComponent中的input属性定义。
1.9 最后的调整
1.9.1 显示文章所属的域名
在article.component.html加
<a class="ui large header" href="{{article.link}}">
{{article.title}}
</a>
<div class="meta">({{article.domain()}})</div>
<ul class="ui big horizontal list voters">
1.9.1 基于分数排序
sortedArticles():Article[]{
return this.articles.sort((a:Article,b:Article)=>b.votes-a.votes);
}
<app-article *ngFor="let article of sortedArticles()" [article]="article">
</app-article>
最后呈现的效果:
2 TypeScript
Angular用TypeScript构建的,TypeScript是ES6的超集。
2.1 类型
相对于ES6,TypeScript最大改善是增加了类型系统。
- 有助于代码的编写,因为它可以在编译期预防Bug
- 有助于代码的阅读,因为它能清晰地表明你的意图
TypeScript中的类型是可选的,ES5用var关键字定义变量,比如var name;而TypeScript仍沿用var定义变量,同时为变量名提供可选的变量类型;
var name:string;
在声明函数时,也可以为函数参数和返回值指定类型:
function greetText(name:string):string{
return "Hello "+name;
}
- 如果不小心让函数返回了一个非string型的返回值,($tsc compile-error.ts)编译器就会告诉我们这里有错误
- 使用该函数的开发人员能很清晰知道自己拿到什么类型数据
尝试REPL,要安装一个小工具,名为TSUN。
$ npm install -g tsun
启动tsun:
$ tsun
这个>是一个命令提示符,表示TSUN已经准备好接收命令了。
2.2 内置类型
2.2.1 字符串
字符串包含文本,声明为string类型:
var name:string="Felipe";
2.2.2 数字
无论整数还是浮点,任何类型的数字都属于number类型,所有数字都是用浮点数表示的,这些数字类型都是number。
var age:number=36;
2.2.3 布尔类型
布尔类型(boolean)以true和false为值
var married:boolean=true;
2.2.4 数组
数组用Array类型表示。然而,因为数组是一组相同数据类型的集合,所有我们还需要为数组中的条目指定一个类型。用Array<type>或者type[]语法来为数组条目指定元素类型。
var jobs:Array<string>=['IBM','Microsoft','Google'];
var jobs:string[]=['Apple','Dell','HP'];
数字型数组的声明与之类似:
var jobs:Array<number>=[1,2,3];
var jobs:number[]=[4,5,6];
2.2.5 枚举
枚举是一组可命名数值的集合。比如,如果我们想拿到某人的一系列角色
enum Role{Employee,Manager,Admin};
var role:Role=Role.Employee;
默认情况下,枚举类型的初始值是0,也可以调整初始化的范围:
enum Role{Employee=3,Manager,Admin};
var role:Role=Role.Employee;
Employee初始值设置为3而不是0,枚举中其他项的值是依次递增的,意味着manager的值为4,admin的值为5。
还可以从枚举的值来反查它的名称:
enum Role{Employee,Manager,Admin};
console.log('Roles:',Role[0],',',Role[1],'and',Role[2]);
2.2.6 任意类型
没有为变量指定类型,它的默认类型就是any。在TypeScript中,any类型的变量能够接收任意类型的数据。
var something:any='as string';
something=1;
something=[1,2,3];
2.2.7 '无'类型
void意味着我们不期望那里有类型。通常用作函数的返回值,表示没有任何返回值
function setName(name:string):void{
this.name=name;
}
2.3 类
ES5采用基于原型的面向对象设计。这种设计模型不使用类,而是依赖于原型。类可以包含属性,方法和构造函数。
class Person{
first_name:string;//属性
last_name:string;
age:number;
constructor(){//构造函数
}
greet(){//方法,返回void类型
console.log("Hello",this.first_name);
}
ageInYears(years:number):number{
return this.age+years;
}
}
调用方法:var p:Person=new Person(); p.age=6;p.ageInYears(12);
2.3.1 继承
面向对象的另一个重要特性就是继承。继承表面子类能够从父类得到它的行为。可以在这个子类中重写、修改以及添加行为。
TypeScript是完全支持继承特性,并不像ES5那样要靠原型链实现。用extends关键字实现。
class Report{
data:Array<string>;
constructor(data:Array<string>){
this.data=data;
}
run(){
this.data.forEach(function(line){console.log(line);})
}
}
var r:Report=new Report(['First line','Second line']);
r.run();
希望有第二报表,需要增加一些头信息和数据,复用现有Report类的run方法来向用户展示数据。
class TabbedReport extends Report{
headers:Array<string>;
constructor(headers:string[],values:string[]){
super(values);
this.headers=headers;
}
run(){
console.log(this.headers);
super.run();
}
}
var headers:string[]=['Name'];
var data:string[]=['Alice Green','Paul Pfifer','Louis Blakenship'];
var r:TabbedReport=new TabbedReport(headers,data);
r.run();
2.4 工具
2.4.1 胖箭头函数
胖箭头(=>)函数是一种快速书写函数的简洁语法。
ES5语法:
var data=['Alice Green','Paul Pfifer','Louis Blakenship'];
data.forEach(function(line){console.log(line);})
可以改为:
var data=['Alice Green','Paul Pfifer','Louis Blakenship'];
data.forEach(line=>console.log(line));
也可以做表达式:
data.forEach(line=>{console.log(line.toUpperCase())});
当只有一个参数时,圆括号可以省略。箭头(=》)语法可以用作表达式:
var events=[2,4,6,8];
var odds=events.map(v=>v+1);
=>重要特性,就是它与环绕它的外部代码共享同一个this;与function不同的是,通常function函数有自己的this。
ES5:
var nate={
name='Nate',
guitars:['Gibson','Martin','Taylor'],
printGuitars:function(){
var self=this;
this.guitars.forEach(function(g){
console.log(self.name+'player a'+g);
});
}
}
现在可以用:
var nate={
name='Nate',
guitars:['Gibson','Martin','Taylor'],
printGuitars:function(){
this.guitars.forEach((g)=>{
console.log(this.name+'player a'+g);
});
}
}
2.4.2 模板字符串
ES6引入了新的模板字符串,它有两大优势:
(1)可以在模板字符串中使用变量。
(2)支持多行字符串。
1、字符串中的变量
也叫字符串插值,可以在字符串插入变量。字符串插值使用反引号。
var firstName='Nate';
var greeting='Hello ${firstName}';
//字符串插值必须用反引号,不能用单引号或双引号
2、多行字符串
反引号字符串是允许多行文本。
var template='<div><h1>hello</h1><p>This is Great WebSite</p></div>';
3 Angular工作原理
一个angular应用就是一棵组件构成的树。由于组件是以树形结构组织起来的,当每个组件被渲染时,它都会递归地渲染下级组件。把页面拆分成组件。原型图创建一个简单的库存管理系统。把页面拆分组件。
- 主导航组件
- 面包屑导航组件
- 产品列表组件
产品列表,一个产品又分为产品图片、产品分类、价格显示。
3.1 产品数据模型
关于Angular,它不要求使用指定的数据模型库。Angular十分灵活,可以支持多种不同的数据模型。
class Product{
constructor{
public sku:string,
public name:string,
public imageUrl:string,
public department:string[],
public price:number
)
}
}
上面代码创建了一个名叫product的类,这个类的构造函数接收5个参数。
3.2 组件
组件是构成Angular应用的基本组成部分。
- 组件注解
- 视图
- 控制器
@Component({
selector:'inventory-app',
template:'
<div class='inventory-app'>
(Products will go here soon)
</div>
'
})
class InventoryApp{
}
@Component被称为注解,紧随其后的类添加一些元数据。
注解明确了下面两项:
- selector(选择器)用来告诉Angular要匹配哪个HTML元素
- template(模板)用来定义视图
组件的控制器是由一个TypeScript类定义的。比如前面代码中的InventoryApp类。
3.2.1 组件selector
selector:'inventory-app'
在HTML使用这个定义的标签:
<inventory-app></inventory-app>
还可以匹配一个以组件名为属性的普通的div元素。
<div inventory-app></div>
3.2.2 组件Template
视图是一个组件中可视的部分。我们可以用@Component中的template配置项来定义组件所用的HTML模板。用到了TypeScript中用反引号包裹的多行文本语法。如果是一个单独的文件,只需把template配置项改为templateUrl配置项。
@Component({
selector: 'inventory-app',
//定义组件所用的HTML模板
template: `
<div class="inventory-app">
(Products will go here soon)
</div>
`
})
3.2.3 添加产品
一般来说,我们应该不会向函数传递超过5个参数,另一个做法将Product类的构造函数修改为接收一个配置对象。
let newProduct =new Product(
'NICEHAT',
'A Nice Black Hat',
'/resources/images/products/black-hat.jpg',
['Men','Accessories','Hats'],
29.99
);
我们希望在界面上展示这个Product。
class InventorApp{
product:Product;
constructor(){
this.product =new Product(
'NICEHAT',
'A Nice Black Hat',
'/resources/images/products/black-hat.jpg',
['Men','Accessories','Hats'],
29
);
}
}
3.2.4 用模板绑定来查看产品
{{....}}语法被称为模板绑定。可以{{count+1}}表达式,{{myFunction(myArguments)}}使用函数返回值。
@Component({
selector: 'inventory-app',
template: `
<div class="inventory-app">
// 花括号中的内容是一个表达式 可以这样{{ count + 1 }}
<h1>{{ product.name }}</h1>
<span>{{ product.sku }}</span>
</div>
`
})
3.2.5 添加更多产品
显示产品列表,Product数组。也可以写Array<Product>。
products: Product[];、
constructor() {
this.products = [
new Product(
'MYSHOES',
'Black Running Shoes',
'/resources/images/products/black-shoes.jpg',
['Men', 'Shoes', 'Running Shoes'],
109.99),
new Product(
'NEATOJACKET',
'Blue Jacket',
'/resources/images/products/blue-jacket.jpg',
['Women', 'Apparel', 'Jackets & Vests'],
238.99),
new Product(
'NICEHAT',
'A Nice Black Hat',
'/resources/images/products/black-hat.jpg',
['Men', 'Accessories', 'Hats'],
29.99)
];
}
3.2.6 选择一个产品
响应用户对产品的选择。
productWasSelected(product: Product): void {
console.log('Product clicked: ', product);
}
3.2.7 用<product-list>列出产品
渲染产品列表。
@Component({
selector: 'inventory-app',
template: `
<div class="inventory-app">
<products-list
[productList]="products"
(onProductSelected)="productWasSelected($event)">
</products-list>
</div>
`
})
3.3 产品列表组件
显示产品列表的组件:
@Component({
selector: 'products-list',
inputs: ['productList'],
outputs: ['onProductSelected'],
template: `
<div class="ui items">
<product-row
*ngFor="let myProduct of productList"
[product]="myProduct"
(click)='clicked(myProduct)'
[class.selected]="isSelected(myProduct)">
</product-row>
</div>
`
})
3.3.1 组件的输入
①
@Component{(
selector:'my-component',
inputs:['name','age']
)}
class MyComponent{
name:string;
age:number;
}
内外属性名称不一样:
inputs:['name:shortName','age:oldAge']
使用组件时,可以这么写:
<my-component [shortName]='myName' [oldAge]="myAge"></my-component>
也可以有其中一个属性不一样:
inputs:['name','age','isEnabled:enabled']
class MyComponent{
name:string;
age:number;
isEnabled:boolean;
}
②
@Component{(
selector:'my-component',
)}
class MyComponent{
@Input() name:string;
@Input() age:number;
}
名字输入属性的内外名字不一样。
@Input('firstname') name:String;
使用组件时,可以这么写:
<my-component [name]='myName' [age]="myAge"></my-component>
注意,name属性对应name输入,也恰好与MyComponent中的name属性对应。
3.3.2 组件的输出
要从组件中把数据传递过去,使用输出绑定。
@Component({
selector:'counter',
template:'{{value}}
<button (click)="increase()">Increase</button>
<button (click)="decrease()"></button>
'
})
class Counter{
value:number;
constructor(){
this.value=1;
}
increase(){
this.value=this.value+1;
return false;
}
decrease(){
this.value=this.value-1;
return false;
}
}
点第一个按钮,调制控制器中的increase()方法,第二按钮时decrease()方法。圆括号属性的语法是:(output)="action"。这个例子中,我们监听按钮的click事件。还有很多内置的事件,如mousedown,mousemove,dbl-click等。
3.3.3 触发自定义事件
- 在@Component配置中,指定outputs配置项
- 在实例属性中,设置一个EventEmitter(事件触发器)
- 在适当的时候,通过EventEmitter触发事件
//子组件
@Compnent({
selector:'single-component';
outputs:[putRingOnIt];
template:`
<button (click)="likes()">Like it?</button>
`
})
class SingleComponent{
putRingOnIt:EventEmitter<string>;
constructor(){
this.putRingOnIt=new EventEmitter();
}
liked():void{
this.putRingOnIt.emit("oh oh oh");
}
}
父组件输出:
//父组件
@Component({
selector:'club',
template:`
<div>
<single-component
(putRingOnIt)="ringWasPlaced($event)",>
</single-component>
`
})
class ClubComponent{
ringWasPlaced(message:string){
console.log(`Put your hands up: ${message}`);
//`${message}` 叫做 字符串插值,即显示message的文本内容
}
}
3.3.4 编写ProductsList的视图模板
(product)="myProduct"是指我们要把myProduct传递给product-row的product输入
(click)='clicked(myProduct)'是表示当元素被点击的时候我们希望做什么。
(class.selected)="isSelected(myProduct)" 设置元素的class属性,如果isSelected返回true,给css增加一个selected类。
@Component({
selector: 'products-list',
inputs: ['productList'],
outputs: ['onProductSelected'],
template: `
<div class="ui items">
<product-row
*ngFor="let myProduct of productList"
[product]="myProduct"
(click)='clicked(myProduct)'
[class.selected]="isSelected(myProduct)">
</product-row>
</div>
`
})
class ProductsList {
/**
* @input productList - the Product[] passed to us
*/
productList: Product[];
/**
* @output onProductSelected - outputs the current
* Product whenever a new Product is selected
*/
onProductSelected: EventEmitter<Product>;
/**
* @property currentProduct - local state containing
* the currently selected `Product`
*/
private currentProduct: Product;
constructor() {
this.onProductSelected = new EventEmitter();
}
clicked(product: Product): void {
this.currentProduct = product;
this.onProductSelected.emit(product);
}
isSelected(product: Product): boolean {
if (!product || !this.currentProduct) {
return false;
}
return product.sku === this.currentProduct.sku;
}
}
3.4 产品条目
host:{'class':'item'}是宿主元素添加为item的css类。
@Component({
selector: 'product-row',
inputs: ['product'],
host: {'class': 'item'},
template: `
<product-image [product]="product"></product-image>
<div class="content">
<div class="header">{{ product.name }}</div>
<div class="meta">
<div class="product-sku">SKU #{{ product.sku }}</div>
</div>
<div class="description">
<product-department [product]="product"></product-department>
</div>
</div>
<price-display [price]="product.price"></price-display>
`
})
class ProductRow {
product: Product;
}
3.5 创建NgModule启动应用
@NgModule({
declarations: [
InventoryApp,
ProductImage,
ProductDepartment,
PriceDisplay,
ProductRow,
ProductsList
],
imports: [ BrowserModule ],
bootstrap: [ InventoryApp ]
})
要访问其他模块的组件,必须满足:
- 和导入组件在同一模块下
- imports导入组件
bootstrap是InventoryApp,告诉程序是以InventoryApp启动的。 因为是浏览器应用,我们必须导入BrowserModule 。启动应用(AOT预编译技术):
platformBrowserDynamic().bootstrapModule(InventoryAppModule);
4 内置指令
4.1 ngIf
<div *ngIf="false"></div>
<div *ngIf="a>b"></div>
<div *ngIf="str=='yes'"></div>
<div *ngIf="myVar!='A'&&myVar!='B'"></div>
<div *ngIf="myFunc()"></div>
4.2 ngSwitch
<div class="container" [ngSwitch]="myVar">
<div *ngSwitchCase="'A'">Var is A</div>
<div *ngSwitchCase="'B'">Var is B</div>
<div *ngSwitchDefault>Var is something else</div>
</div>
可以匹配多个值:
<div class="ui raised segment" [ngSwitch]="choice">
<div *ngSwitchCase="1">First Choice</div>
<div *ngSwitchCase="2">Second Choice</div>
<div *ngSwitchCase="2">Second Choice,again</div>
<div *ngSwitchCase="3">Third Choice</div>
<div *ngSwitchDefault>Var is something else</div>
</div>
4.3 ngStyle
给特定的DOM元素设定CSS属性。以下是把CSS的backgroud-color属性设置为字符串字面量的yellow。
<div [style.background-color]='yellow'>
Users fixed yellow backgroud
</div>
另一种设置固定值方式使用ngStyle属性,使用键值对设置每个属性。
<div [ngStyle]="{color:'white','background-color:':'blue'}">
Users fixed white text on blue background
</div>
因为color是合法的键,而background-color存在连字符,不允许存对象的键名当中,除非是一个字符串。 因此用了引号。
ngStyle基于动态值调整,基于输入框的值设定字体大小。
<div>
<span [ngStyle]="{color:'red'}" [style.font-size.px]="fontSize">
red text
</span>
</div>
px可以替换成em或者%。
设置文字颜色和背景颜色:
<div>
<span [ngStyle]="{color:color}">
{{color}} text</span>
</div>
<div>
<span [style.background-color]="color">
{{color}} background</span>
</div>
4.4 ngClass
动态设置和改变一个给定Dom元素的CSS类。传入一个对象字面量,该对象希望以类名作为键,而值应该是一个用来表明是否应该应用该类的真假值。
.bordered{
border:1px dashed black;
backgroud-color:#eee;
}
<div [ngClass]="{bordered:false}">不用bordered</div>
<div [ngClass]="{bordered:true}">用bordered</div>
添加一个变量作为对象值。
<div [ngClass]="{bordered:isBordered}">
isBordered是true还是false?
</div>
也可以这样:
export class NgClassSampleApp{
isBordered:boolean;
classesObj:Object;
}
<div [ngClass]="classesObj">
</div>
出现连字符要用引号:<div [ngClass]="{'bordered-box':false}"></div>
传入一个数组型字面量:
<div class="base" [ngClass]="['blue','round']"></div>
也可以定义this.classList=['blue','round'];传入[ngClass]="classList";
4.5 ngFor
语法就是:
<tr *ngFor="let p of people">
<td>{{ p.name }}</td>
<td>{{ p.age }}</td>
<td>{{ p.city }}</td>
</tr>
嵌套查询:
<div *ngFor="let item of peopleByCity">
<h2 class="ui header">{{ item.city }}</h2>
<table class="ui celled table">
<thead>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
</thead>
<tr *ngFor="let p of item.people">
<td>{{ p.name }}</td>
<td>{{ p.age }}</td>
</tr>
</table>
</div>
获取索引:
<div class="ui list" *ngFor="let c of cities; let num = index">
<div class="item">{{ num+1 }} - {{ c }}</div>
</div>
4.6 ngNonBindable
假如我们想渲染纯文本{{content}}。通常情况下,这段文本被绑定到变量content的值,但是我们能想div渲染变量content的内容。
@Component({
selector: 'ng-non-bindable-sample-app',
template: `
<div class='ngNonBindableDemo'>
<span class="bordered">{{ content }}</span>
<span class="pre" ngNonBindable>
← This is what {{ content }} rendered
</span>
</div>
`
})
export class NgNonBindableSampleApp {
content: string;
constructor() {
this.content = 'Some text';
}
}
第一个输出Some text。第二直接输出{{context}}。
5 表单
5.2 FormControl和FormGroup
5.2.1 FormControl
FormControl代表单一的输入字段。
封装这些字段的值和状态,是否有效,是否脏读或者是否有误。
<input type="text" [formControl]="name">
let nameControl=new FormControl("Nate");
let name=nameControl.value;
5.2.2 FormGroup
FormGroup表示多个FormControl。
let personInfo=new FormGroup({
firstName:new FormControl("Nate"),
lastName:new FormControl("Murray"),
zip:new FormControl("90210"),
});
let value=personInfo.value;//->{firstName:"Nate",....}
5.3 第一个表单
创新表单的方式很多。
5.3.1 加载FormsModulr
使用FormsModule以及ReativeFormsModule。
import {
FormsModule,
ReactiveFormsModule
} from '@angular/forms';
@NgModule({
declarations: [
FormsDemoApp,
DemoFormSku,
],
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule
],
bootstrap: [ FormsDemoApp ]
})
FormsModule为我们提供了一些模板驱动的指令,例如ngModel,NgForm。而ReactiveFormsModule提供了下列指令,formControl,ngFormGroup。
此外,还有很多指定。
5.3.2 简单SKU表单:@Component注解
import {Component} from "@angular/core";
就可以用:
@Component({
selector:"demo-form-sku",
5.3.3 简单SKU表单:template
template: `
<div class="ui raised segment">
<h2 class="ui header">Demo Form: Sku</h2>
<form #f="ngForm"
(ngSubmit)="onSubmit(f.value)"
class="ui form">
<div class="field">
<label for="skuInput">SKU</label>
<input type="text"
id="skuInput"
placeholder="SKU"
name="sku" ngModel>
</div>
<button type="submit" class="ui button">Submit</button>
</form>
</div>
`
1.form和NgForm
导入了FormsModule,当这些指令在视图可用时,就会自动附加到任何匹配其selector的节点上。选择器包括form标签,NgForm就会自动附加到视图中的<form>标签中。NgForm提供了两个重要的功能:
- 一个名叫ngForm的FormGroup对象
- 一个输出事件(ngSubmit)
<form #f="ngForm"
(ngSubmit)="onSubmit(f.value)"
class="ui form">
声明一个局部变量#f="ngForm"。#v=thing语法就是在当前视图中创建一个局部变量。ngForm创建了一个别名,并绑定到变量#f。这个ngForm是由NgForm指令导出的。它是FormGroup类型的。上面NgForm会自动附加到<form>标签上。
- (ngSubmit):来自NgForm指令
- onSubmit():将会在我们组件类中进行定义
-
f.value:f就是FormGroup,而.value会以键值对的形式返回FormGroup所有控件的值。
2.input和NgModel
在讨论NgModule之前,关于input标签。
- classs="ui form"和class="field",它们来自CSS框架的Semantic UI。
- label标签的for属性和Input标签的id属性是一致的。
NgModel指令指定的selector是ngModel。
- 单向数据绑定
- 创建一个名叫sku的FormControl(这个sku来自与input标签上的name属性)
NgModel会创建一个新的FormControl对象,自动添加到父FormGroup上,把这个FormControl对象绑定一个DOM上。它会在视图中的Input标签和FormControl对象之间建立关联。NgModel与ngModel,前者指的是类和供代码中引用的对象。后者指的来自指令的选择器selector,只会被用在DOM/模板中。NgModel和FormControl并不是同一个。NgModel是用在视图中的指令,而FormControl用来表示表单中的数据和验证规则。
5.3.4 简单SKU表单:组件定义类
export class DemoFormSku {
onSubmit(form: any): void {
console.log('you submitted value:', form);
}
}
5.4 FormBuilder
使用ngForm和ngControl隐式构建FormControl和FormGroup。但无法为我们提供更多定制化选项,使用FormBuilder构建表单是一种更为灵活和通用的方式。
表单是由FormControl和FormGroup创建的,而FormBuilder可以帮助我们创建它们。
5.4.1 响应式表单FormBuilder
导入相应的类。
import { Component } from '@angular/core';
import {
FormBuilder,
FormGroup
} from '@angular/forms';
@Component({
selector: 'demo-form-sku-builder',
5.4.2 使用FormBuilder
通过组建类上声明带参数的constructor,注入了一个FormBuilder。
export class DemoFormSkuBuilder{
myForm:FormGroup;
constructor(fb:FormBuilder){//依赖注入
this.myForm=fb.group({
'sku':['ABC123']
});
}
}
onSubmit(value:string):void{
console.log("you submitted value:",value);
}
在这期间,Angular会注入一个从FormBuilder类创建的对象实例,并把它赋值给fb变量。
我们将会使用FormBuilder中的两个主要函数:
- control,用于创建一个新的FormControl
- group,用于创建一个新的FormGroup
我们在类中创建一个名叫myForm的实例变量。myForm是FormGroup类型,通常调用fb.group()来创建FormGroup。.group方法的参数是代表组内各个FormControl的键值对。设置一个名叫sku的控件,其值为["ABC123"]。默认值为"ABC123"。
5.4.3 在视图中使用myForm
希望修改<form>标签,使用myForm变量。当导入FormModule时,ngForm就会自动起作用。还提到ngForm会自动创建自己的FormGroup。而不希望使用外部的FormBuilder,使用FormBuilder创建的这个myForm实例变量。Angular提供了另一个指令,能让我们使用现有的FormGroup,叫做formGroup。
<h2 class="ui header">Demo Form: Sku with Builder</h2>
<form [formGroup]="myForm"
还需要onSubmit的f替换为myForm,因为现在的myForm变量中保存着表单的配置和值。想让程序运行起来。将FormControl绑定到input标签上,ngControl会创建一个新的FormControl对象,并附加到父FormGroup。已经用了FormBuilder创建了自己的FormControl。
<input type="text"
id="skuInput"
placeholder="SKU"
[formControl]="myForm.controls['sku']">
将input标签上的formControl指令了myForm.control上现有的FormControl控件sku。
5.4.4 代码示例
import { Component } from '@angular/core';
import {
FormBuilder,
FormGroup
} from '@angular/forms';
@Component({
selector: 'demo-form-sku-builder',
template: `
<div class="ui raised segment">
<h2 class="ui header">Demo Form: Sku with Builder</h2>
<form [formGroup]="myForm"
(ngSubmit)="onSubmit(myForm.value)"
class="ui form">
<div class="field">
<label for="skuInput">SKU</label>
<input type="text"
id="skuInput"
placeholder="SKU"
[formControl]="myForm.controls['sku']">
</div>
<button type="submit" class="ui button">Submit</button>
</form>
</div>
`
})
export class DemoFormSkuBuilder {
myForm: FormGroup;
constructor(fb: FormBuilder) {
this.myForm = fb.group({
'sku': ['ABC123']
});
}
onSubmit(value: string): void {
console.log('you submitted value: ', value);
}
}
隐式创建新的FormGroup和FormControl,使用:
-
ngForm
-
ngModel
绑定一个现有FormGroup和FormControl,使用:
- formGroup
- formControl
5.4 添加验证
用户输入的数据格式并不正确的,如果有人输入错误的数据格式。并阻止他提交表单。验证器由Validators模块提供。Validators.required是最简单的验证,表明指定的字段是必填项,否则就认为这个FormControl是无效的。
- 为FormControl对象指定一个验证器
- 在视图中检查验证器的状态,并据此采取行动
要为FormControl对象分配一个验证器,我们可以直接把它作为第二个参数传给formControl的构造函数。
let control=new FormControl('sku',Validators.required);
使用FormBuilder。
constructor(fb: FormBuilder) {
this.myForm = fb.group({
'sku': ['', Validators.required]
});
this.sku = this.myForm.controls['sku'];
}
- 可以显式地把sku这个FormControl赋值给类的实例变量。
- 可以在myForm中查找sku这个FormControl,这样简化组化类的工作
5.4.1 显示地把sku设置实例变量
在视图中,处理单个FormControls的最灵活的方式是将每个FormControl都定义组件里,把sku定义类上如下所示。
export class DemoFormWithValidationsExplicit {
myForm: FormGroup;
sku: AbstractControl;
constructor(fb: FormBuilder) {
this.myForm = fb.group({
'sku': ['', Validators.required]
});
this.sku = this.myForm.controls['sku'];
}
onSubmit(value: string): void {
console.log('you submitted value: ', value);
}
}
- 在类的顶部设置sku:AbstractControl
- 把用FormBuilder创建的myForm赋值给this.sku变量
现在sku可以得到验证,要以不同方式的把它用在视图中:
- 检查整个表单的有效性并显示一条错误信息
- 检查单个字段的有效性并显示一条错误信息
- 检查单个字段的有效性,当字段无效时将字段显示为红色
- 检查单个字段在特定规则下的有效性并显示一条错误信息。
1、表单信息
myForm.Valid来检查整个表单的有效性。
<div *ngIf="!myForm.vaild" class="ui error message">is invaild</div>
2、字段信息
sku.Valid为字段显示错误信息。
<div *ngIf="sku.hasError('required')" class="ui error message">is invaild</div>
3、字段着色
这里用的CSS类的error,当给<div class='field'>节点加入CSS类error时,这个输入就会带有红色的边框。用户修改表单后才显示错误状态。
<div class="field" [class.error]="!sku.valid && sku.touched">
4、特定验证
可能很多原因导致一个表单字段无效,对于失败的验证,通常希望根据不同的原因显示不同信息。可以用hasError方法检查特定的验证失败。
<div *ngIf="sku.hasError('required')" class="ui error message">SKU is Required</div>
5、整合
import { Component } from '@angular/core';
import {
FormBuilder,
FormGroup,
Validators,
AbstractControl
} from '@angular/forms';
@Component({
selector: 'demo-form-with-events',
template: `
<div class="ui raised segment">
<h2 class="ui header">Demo Form: with events</h2>
<form [formGroup]="myForm"
(ngSubmit)="onSubmit(myForm.value)"
class="ui form">
<div class="field"
[class.error]="!sku.valid && sku.touched">
<label for="skuInput">SKU</label>
<input type="text"
class="form-control"
id="skuInput"
placeholder="SKU"
[formControl]="sku">
<div *ngIf="!sku.valid"
class="ui error message">SKU is invalid</div>
<div *ngIf="sku.hasError('required')"
class="ui error message">SKU is required</div>
</div>
<div *ngIf="!myForm.valid"
class="ui error message">Form is invalid</div>
<button type="submit" class="ui button">Submit</button>
</form>
</div>
`
})
export class DemoFormWithEvents {
myForm: FormGroup;
sku: AbstractControl;
constructor(fb: FormBuilder) {
this.myForm = fb.group({
'sku': ['', Validators.required]
});
this.sku = this.myForm.controls['sku'];
this.sku.valueChanges.subscribe(
(value: string) => {
console.log('sku changed to:', value);
}
);
this.myForm.valueChanges.subscribe(
(form: any) => {
console.log('form changed to:', form);
}
);
}
onSubmit(form: any): void {
console.log('you submitted value:', form.sku);
}
}
6、移除sku实例变量
在上面的例子,将sku:AbstractControl设置为一个实例变量。通常,不希望一个AbstractControl控件创建一个变量。
<input type="text" id="skuInput" placeHolder="SKU" [formControl]="myForm.controls['sku']">
<div *ngIf="!myForm.controls['sku'].valid" class="ui error Message">SKU is required</div>
<div *ngIf="!myForm.controls['sku'].hasError('required')" class="ui error Message">SKU is required</div>
5.4.2 自定义验证器
经常要写自定义验证器。要明白如何实现自己的验证器
export class validators{
static required(c:FormControl):StringMap<string,boolean>{
return isBlack(c.value)||c.value==""?{"required":true}:null;
}
}
- 接收一个FormControl作为输入
- 当验证失败时,会返回一个StringMap<string,boolean>对象,它的键是“错误代码”,值为true
1、编写验证器
假设我们的sku有持续的验证需求。比如sku必须以123作为开始。
function skuValidatior(control:FormControl):{[s:string]:boolean}{
if(!control.value.match('/^123')){
return {invalidSku:true};
}
}
2、给FormControl分配验证器
要为FormControl添加特性。sku已经有一个验证器。可以用Validators.compose来实现。
constructor(fb:FormBuilder){
this.myForm=fb.group({
'sku':['',Validators.compose([
Validarirs.required,skuValidator
])]
});
}
Validators.compose把两个验证器包装在一起,可以将其赋值给FormControl,只有两个验证都合法时,FormControl才是合法的。
<div *ngIf="sku.hasError('invalidSku')"
class="ui error message">SKU must begin with <span>123</span></div>
5.5 监听变化
提交表单时才调用onSubmit方法来获取表单的值,也要经常监听控件的变化。
FormGroup和FormControl都带有EventEmitter(事件发射器),可以通过它来观察变化。
- 通过调用control.valueChanges访问到这个EventEmitter;
- 然后使用.subscribe方法添加一个监听器
constructor(fb: FormBuilder) {
this.myForm = fb.group({
'sku': ['', Validators.required]
});
this.sku = this.myForm.controls['sku'];
this.sku.valueChanges.subscribe(
(value: string) => {
console.log('sku changed to:', value);
}
);
this.myForm.valueChanges.subscribe(
(form: any) => {
console.log('form changed to:', form);
}
);
}
监听两个事件:sku字段的变化和整个表单的变化。我们传递了一个带有next键的对象。next就是我们希望当值发生变化时被调用的函数。如果在输入框中输入kj,就会在控制台看到:
sku changed to : k
form changed to : Object {sku:"k"}
sku changed to : kj
form changed to : Object {sku:"kj"}
监听单个FormControl时,就会得到一个值;监听整个表单时,就会得到一个包含键值对的对象。
5.6 ngModel
ngModel是一个特殊的指令,它将模型绑定到表单中。ngModel的特殊之处在于它实现了双向绑定。Angular通常的数据流向是单向的自顶向下,但对于表单来说,双向绑定有时更容易。
export class DemoFormNgModel {
myForm: FormGroup;
productName: string;
constructor(fb: FormBuilder) {
this.myForm = fb.group({
'productName': ['', Validators.required]
});
}
onSubmit(value: string): void {
console.log('you submitted value: ', value);
}
}
Input标签使用ngModel。
<label for="productNameInput">Product Name</label>
<input type="text" id="productNameInput" placeholder="Product Name"
[formControl]="myForm.get('productName')" [(ngModel)]="productName">
ngModel属性上同时()和[]。即使用了表示输入属性(@Input)的方括号[],又使用了表示输出属性(@Output)的圆括号(),这就是双向绑定的标志。仍然用FormControl指定此input应该绑定到表单上的FormControl。这是因为ngModel只负责将input绑定到对象实例上,但FormControl的功能是与此独立的。由于我们需要对这个值加以验证并以把它作为表单的一部分提交上去,保留formControl指令。
<div class="ui info message">
The product name is: {{productName}}
</div>
6 HTTP
处理异步代码比处理同步代码更加棘手。在JavaScript中,通常有3种处理异步代码的方式:
- 回调(callback)
- 承诺(promise)
- 可观察对象(observable,在angular最佳方式)
例在中,我们将:
- 展示一个Http的基本例子
- 创建一个随敲随搜组件用于搜索YouTube
- 讨论Http库的API细节
6.1 使用@angular/http
HTTP在Angular中被拆分为一个单独模块。
import {
Http,
Response,
RequestOptions,
Headers
} from '@angular/http';
从@angular/http导入
导入HttpModule,是便于使用的模块集合。
import {
Component
} from '@angular/core';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { HttpModule } from '@angular/http';
把HttpModule作为依赖项,加入NgModule的imports列表之中。
@NgModule({
declarations: [
HttpApp,
SimpleHTTPComponent,
MoreHTTPRequests,
YouTubeSearchComponent,
SearchBox,
SearchResultComponent
],
imports: [
BrowserModule,
HttpModule // <--- right here
],
bootstrap: [ HttpApp ],
providers: [
youTubeServiceInjectables
]
})
就可以把Http服务注入组件中了。
constructor(private http: Http) {
}
6.2 基本需求
做的就是向jsonplaceholder API发起一个简单的GET请求。
- 有一个调用makeRequest的button
- makeRequest会调用http库向API发起一个GET请求
- 当请求返回时,使用返回结果中的数据更新this.data
6.3.1 构建SimpleHTTPComponent的@Component
导入一些模块,指定@Component的selector。
import { Component } from '@angular/core';
import { Http,Response } from '@angular/http';
@Component({
selector:'simple-http',
6.3.2 构建SimpleHTTPComponent的Template
@Component({
selector: 'simple-http',
template: `
<h2>Basic Request</h2>
<button type="button" (click)="makeRequest()">Make Request</button>
<div *ngIf="loading">loading...</div>
<pre>{{data | json}}</pre>
`
模板中有三个有趣的部分:
- button
- 载入指示器
- data
将控制器中的makeRequest函数绑定到button的(click)上。
6.3.3 构建SimpleHTTPComponent的控制器
export class SimpleHTTPComponent {
data: Object;
loading: boolean;
有了data和loading这个两个实例变量。分别用来存储API返回的数据值与表示加载状态。注入关键模块Http。
需要记住,当我们public http:Http中使用public关键字的时候,TypeScript会将http赋值给this.http。
constructor(private http:Http){
this.http=http;
}
makeRequest函数发起第一个Http请求。
makeRequest(): void {
this.loading = true;
this.http.request('http://jsonplaceholder.typicode.com/posts/1')
.subscribe((res: Response) => {
this.data = res.json();
this.loading = false;
});
}
调用makeRequest时,首先设置this.loading=true。这会在页面上显示载入指示器。httpRequest会返回一个observable对象。可以使用subscribe订阅变化。
this.http.request('http://jsonplaceholder.typicode.com;/posts/1').subscribe((res:Response)=>{
})
当http.request返回一个流时,它就会发出Response对象,用json方法提取响应体并解析一个Object,然后将这个Object赋值给this.data。
6.3.4 完整的SimpleHTTPComponent
import {Component} from '@angular/core';
import {Http, Response} from '@angular/http';
@Component({
selector: 'simple-http',
template: `
<h2>Basic Request</h2>
<button type="button" (click)="makeRequest()">Make Request</button>
<div *ngIf="loading">loading...</div>
<pre>{{data | json}}</pre>
`
})
export class SimpleHTTPComponent {
data: Object;
loading: boolean;
constructor(private http: Http) {
}
makeRequest(): void {
this.loading = true;
this.http.request('http://jsonplaceholder.typicode.com/posts/1')
.subscribe((res: Response) => {
this.data = res.json();
this.loading = false;
});
}
}
6.4 编写YouTubeSearchComponent
从代码中获取API服务器上数据的最简方式。这一节出,会打造一个随着输入搜索YouTube组件。当搜索结果返回时,通过一个列表来展示每一个视频的缩略图、描述和链接。
6.4.1 编写SearchResult
class SearchResult {
id: string;
title: string;
description: string;
thumbnailUrl: string;
videoUrl: string;
constructor(obj?: any) {
this.id = obj && obj.id || null;
this.title = obj && obj.title || null;
this.description = obj && obj.description || null;
this.thumbnailUrl = obj && obj.thumbnailUrl || null;
this.videoUrl = obj && obj.videoUrl ||
`https://www.youtube.com/watch?v=${this.id}`;
}
}
6.4.2 编写YouTubeService
为了使用这个API,你需要API密钥。我们在示例代码中包含了一个可控大家使用的API密钥。
YouTubeService设置两个用来表示API密钥和API URL的常量。
let YOUTUBE_API_KEY:string="XXX_YOUR_KEY_HERE_XXX";
let YOUTUBE_API_URL:string="https://www.googleapis.com/youtube/v3/search";
最后,还要测试一下应用。我们并不希望在产品环境下进行测试,而希望测试试预产生产或开发阶段的API。