目录
3.8 依赖注入(Dependency Injection)
7.5 HTML 属性(Attribute)、 class 和 style 绑定
1 Angular介绍
Angular是谷歌开发的一款开源的web前端框架,诞生于2009年,由Misko Hevery 等人创建,后为Google所收购。是一款优秀的前端JS框架,已经被用于Google的多款产品当中。 根据项目数统计angular(1.x 、2.x 、4.x、5.x、6.x、7.x 、8.x、9.x)是现在网上使用量最大的框架。
Angular基于TypeScript和react、vue相比 Angular更适合中大型企业级项目。
2 Angular目录结构分析
app.module.ts详解、以及Angular中创建组件、组件详解、 绑定数据
2.1 目录结构分析
|--E2e 应用的端对端(e2e)测试,用 Jasmine 写成并用 protractor 端对端测试运行器测试。
|--Node_modules 依赖包
|--Src
|--App Angular应用文件
|--App.module.ts
|---App.component.ts
|--assets 资源文件
|--environments 环境配置:开发、部署
|--index.html 应用的宿主页面。 它以特定的顺序加载一些基本脚本。 然后它启动应用,将根AppComponent放置到自定义<my-app>标签里。
|--main.ts 项目的入口文件
|--polyfills.ts 处理浏览器兼容问题
|--angular.json Cli配置文件
|--.editorconfig 统一代码风格工具配置,不支持的需要安装插件
|--.gitignore Git配置文件
|--karma.conf.js 在测试指南中提到的 karma 测试运行器的配置。
|--package.json 项目指定npm依赖包
|--tsconfig.app.json Typescript编译配置
|--tsconfig.spec.json Typescript测试编译配置
|--tsconfig.json Typescript编译配置
|--tslint.json Typescript语法检查器
详情参考:https://www.angular.cn/guide/file-structure
2.2 app.module.ts、组件分析
定义 AppModule,这个根模块会告诉 Angular 如何组装该应用。 目前,它只声明了 AppComponent。 稍后它还会声明更多组件。
2.3 自定义组件
https://cli.angular.io/
创建组件内容详解:ng g component components/header
2.4 app.component.ts组件分析
Angular应用中,模板指的的是@Component装饰器的template或templateUrl指向的HTML页面 例如:
import { Component } from '@angular/core';
interface Course {
id:number,
description:string
}
@Component({
selector: 'app-root',
// templateUrl: './app.component.html',
template:`
<div class="course">
<span class="description">{{courseObj.description}}</span>
</div>
`,
styleUrls: ['./app.component.css']
})
export class AppComponent{
title = 'ng-module-routes';
id:number = 1;
description:string = 'sss';
public courseObj: Course = {
id: 1,
description: "Angular For Beginners"
};
}
很明显Angular不是简单地用一个字符串来处理模板。 那么这是如何工作的?
Angular不会生成HTML字符串,它直接生成DOM数据结构
实际上,Angular把组件类中的数据模型应用于一个函数(DOM component renderer)。 该函数的输出是对应于此HTML模板的DOM数据结构。
一旦数据状态发生改变,Angular数据检测器检测到,将重新调用 该DOM component renderer。
Mvvm定义是Model-View-ViewModel的简写。即模型-视图-视图模型。
- 【模型】指的是后端传递的数据。
- 【视图】指的是所看到的页面。
- 【视图模型】mvvm模式的核心,它是连接view和model的桥梁。
它有两个方向:
- 一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。
- 二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。
实现的方式是:DOM 事件监听。这两个方向都实现的,我们称之为数据的双向绑定。
在MVVM的框架下视图和模型是不能直接通信的。它们通过ViewModel来通信,ViewModel通常要实现一个observer观察者,当数据发生变化,ViewModel能够监听到数据的这种变化,然后通知到对应的视图做自动更新,而当用户操作视图,ViewModel也能监听到视图的变化,然后通知数据做改动,这实际上就实现了数据的双向绑定。
并且MVVM中的View 和 ViewModel可以互相通信。MVVM流程图如下:
3 Angular 架构
Angular 2 应用程序应用主要由以下 8 个部分组成:
- 1、模块 (Modules)
- 2、组件 (Components)
- 3、模板 (Templates)
- 4、元数据 (Metadata)
- 5、数据绑定 (Data Binding)
- 6、指令 (Directives)
- 7、服务 (Services)
- 8、依赖注入 (Dependency Injection)
下图展示了每个部分是如何相互工作的:
图中的模板 (Templates)是由 Angular 扩展的 HTML 语法组成,组件 (Components)类用来管理这些模板,应用逻辑部分通过服务 (Services)来完成,然后在模块中打包服务与组件,最后通过引导根模块来启动应用。
接下来对以上 8 个部分分开解析:
3.1 模块 (Modules)
模块由一块代码组成,可用于执行一个简单的任务。
Angular 应用是由模块化的,它有自己的模块系统:NgModules。
每个 Angular 应该至少要有一个模块(根模块),一般可以命名为:AppModule。
Angular 模块是一个带有 @NgModule 装饰器的类,它接收一个用来描述模块属性的元数据对象。
几个重要的属性如下:
-
declarations (声明) - 视图类属于这个模块。 Angular 有三种类型的视图类: 组件 、 指令 和 管道 。
-
exports - 声明( declaration )的子集,可用于其它模块中的组件模板 。
-
imports - 本模块组件模板中需要由其它导出类的模块。
-
providers - 服务的创建者。本模块把它们加入全局的服务表中,让它们在应用中的任何部分都可被访问到。
-
bootstrap - 应用的主视图,称为根组件,它是所有其它应用视图的宿主。只有根模块需要设置 bootstrap 属性中。
一个最简单的根模块 app/app.module.ts 文件:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
@NgModule({
imports: [ BrowserModule ],
providers: [ Logger ],
declarations: [ AppComponent ],
exports: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
接下来我们通过引导根模块来启动应用,开发过程通常在 main.ts 文件中来引导 AppModule ,app/main.ts 文件:
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
3.2 组件(Components)
组件是一个模板的控制类用于处理应用和逻辑页面的视图部分。
组件是构成 Angular 应用的基础和核心,可用于整个应用程序中。
组件知道如何渲染自己及配置依赖注入。
组件通过一些由属性和方法组成的 API 与视图交互。
创建 Angular 组件的方法有三步:
- 从 @angular/core 中引入 Component 修饰器
- 建立一个普通的类,并用 @Component 修饰它
- 在 @Component 中,设置 selector 自定义标签,以及 template 模板
3.3 模板(Templates)
Angular模板的默认语言就是HTML。
我们可以通过使用模板来定义组件的视图来告诉 Angular 如何显示组件。以下是一个简单是实例:
<div>
网站地址 : {{site}}
</div>
在Angular中,默认使用的是双大括号作为插值语法,大括号中间的值通常是一个组件属性的变量名。
3.4 元数据(Metadata)
元数据告诉 Angular 如何处理一个类。
考虑以下情况我们有一个组件叫作 Component ,它是一个类,直到我们告诉 Angular 这是一个组件为止。
你可以把元数据附加到这个类上来告诉 Angular Component 是一个组件。
在 TypeScript 中,我们用 装饰器 (decorator) 来附加元数据。
@Component({
selector : 'mylist',
template : '<h2>教程</h2>'
directives : [ComponentDetails]
})
export class ListComponent{...}
@Component 装饰器能接受一个配置对象,并把紧随其后的类标记成了组件类。
Angular 会基于这些信息创建和展示组件及其视图。
@Component 中的配置项说明:
-
selector - 一个 css 选择器,它告诉 Angular 在 父级 HTML 中寻找一个 <mylist> 标签,然后创建该组件,并插入此标签中。
-
templateUrl - 组件 HTML 模板的地址。
-
directives - 一个数组,包含 此 模板需要依赖的组件或指令。
-
providers - 一个数组,包含组件所依赖的服务所需要的依赖注入提供者。
3.5 数据绑定(Data binding)
数据绑定为应用程序提供了一种简单而一致的方法来显示数据以及数据交互,它是管理应用程序里面数值的一种机制。
通过这种机制,可以从HTML里面取值和赋值,使得数据的读写,数据的持久化操作变得更加简单快捷。
如图所示,数据绑定的语法有四种形式。每种形式都有一个方向——从 DOM 来、到 DOM 去、双向,就像图中的箭头所示意的。
插值 : 在 HTML 标签中显示组件值。
<h3>
{{title}}
<img decoding="async" src="{{ImageUrl}}">
</h3>
属性绑定: 把元素的属性设置为组件中属性的值。
<img [src]="userImageUrl">
事件绑定: 在组件方法名被点击时触发。
<button (click)="onSave()">保存</button>
双向绑定: 使用Angular里的NgModel指令可以更便捷的进行双向绑定。
<input [value]="currentUser.firstName"
(input)="currentUser.firstName=$event.target.value" >
3.6 指令(Directives)
Angular模板是动态的 。当 Angular 渲染它们时,它会根据指令对 DOM 进行修改。
指令是一个带有"指令元数据"的类。在 TypeScript 中,要通过 @Directive 装饰器把元数据附加到类上。
在Angular中包含以下三种类型的指令:
- 属性指令:以元素的属性形式来使用的指令。
- 结构指令:用来改变DOM树的结构
- 组件:作为指令的一个重要子类,组件本质上可以看作是一个带有模板的指令。
<li *ngFor="let site of sites"></li>
<site-detail *ngIf="selectedSite"></site-detail>
*ngFor 告诉 Angular 为 sites 列表中的每个项生成一个 <li> 标签。
*ngIf 表示只有在选择的项存在时,才会包含 SiteDetail 组件。
3.7 服务(Services)
Angular2中的服务是封装了某一特定功能,并且可以通过注入的方式供他人使用的独立模块。
服务分为很多种,包括:值、函数,以及应用所需的特性。
例如,多个组件中出现了重复代码时,把重复代码提取到服务中实现代码复用。
以下是几种常见的服务:
- 日志服务
- 数据服务
- 消息总线
- 税款计算器
- 应用程序配置
以下实例是一个日志服务,用于把日志记录到浏览器的控制台:
export class Logger {
log(msg: any) { console.log(msg); }
error(msg: any) { console.error(msg); }
warn(msg: any) { console.warn(msg); }
}
3.8 依赖注入(Dependency Injection)
控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫"依赖查找"(Dependency Lookup)。
通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。
在传统的开发模式中,调用者负责管理所有对象的依赖,循环依赖一直是梦魇,而在依赖注入模式中,这个管理权交给了注入器(Injector),它在软件运行时负责依赖对象的替换,而不是在编译时。这种控制反转,运行注入的特点即是依赖注入的精华所在。
Angular 能通过查看构造函数的参数类型,来得知组件需要哪些服务。 例如, SiteListComponent 组件的构造函数需要一个 SiteService:
constructor(private service: SiteService) { }
当 Angular 创建组件时,会首先为组件所需的服务找一个注入器( Injector ) 。
注入器是一个维护服务实例的容器,存放着以前创建的实例。
如果容器中还没有所请求的服务实例,注入器就会创建一个服务实例,并且添加到容器中,然后把这个服务返回给 Angular 。
当所有的服务都被解析完并返回时, Angular 会以这些服务为参数去调用组件的构造函数。 这就是依赖注入 。
4 Angular 数据显示
本节将介绍如何将数据显示到用户界面上,可以使用以下三种方式:
- 通过插值表达式显示组件的属性
- 通过 NgFor 显示数组型属性
- 通过 NgIf 实现按条件显示
4.1 通过插值表达式显示组件的属性
要显示组件的属性,插值是最简单的方式,格式为:{{属性名}}。
app/app.component.ts 文件:
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
<h1>{{title}}</h1>
<h2>我喜欢的网站: {{mySite}}</h2>
`
})
export class AppComponent {
title = '站点列表';
mySite = 'Angular教程';
}
Angular 会自动从组件中提取 title 和 mySite 属性的值,并显示在浏览器中
注意:模板是包在反引号 (`) 中的一个多行字符串,而不是单引号 (')。
4.2 使用 ngFor 显示数组属性
我们也可以循环输出多个站点,修改以下文件:
app/app.component.ts 文件:
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
<h1>{{title}}</h1>
<h2>我喜欢的网站: {{mySite}}</h2>
<p>网站列表:</p>
<ul>
<li *ngFor="let site of sites">
{{ site }}
</li>
</ul>
`
})
export class AppComponent {
title = '站点列表';
sites = ['菜鸟教程', 'Google', 'Taobao', 'Facebook'];
mySite = this.sites[0];
}
代码中我们在模板使用 Angular 的 ngFor 指令来显示 sites 列表中的每一个条目,不要忘记 *ngFor 中的前导星号 (*) 。。
修改后,浏览器显示如下所示:
实例中 ngFor 循环了一个数组, 事实上 ngFor 可以迭代任何可迭代的对象。
接下来我们在 app 目录下创建 site.ts 的文件,代码如下:
app/site.ts 文件:
export class Site {
constructor(
public id: number,
public name: string) { }
}
以上代码中定义了一个带有构造函数和两个属性: id 和 name 的类。
接着我们循环输出 Site 类的 name 属性:
app/app.component.ts 文件:
import { Component } from '@angular/core';
import { Site } from './site';
@Component({
selector: 'my-app',
template: `
<h1>{{title}}</h1>
<h2>我喜欢的网站: {{mySite.name}}</h2>
<p>网站列表:</p>
<ul>
<li *ngFor="let site of sites">
{{ site.name }}
</li>
</ul>
`
})
export class AppComponent {
title = '站点列表';
sites = [
new Site(1, '菜鸟教程'),
new Site(2, 'Google'),
new Site(3, 'Taobao'),
new Site(4, 'Facebook')
];
mySite = this.sites[0];
}
修改后,浏览器显示如下所示:
4.3 通过 NgIf 进行条件显示
我们可以使用 NgIf 来设置输出指定条件的数据。
以下实例中我们判断如果网站数 3 个以上,输出提示信息:代码如下:
app/app.component.ts 文件:
import { Component } from '@angular/core';
import { Site } from './site';
@Component({
selector: 'my-app',
template: `
<h1>{{title}}</h1>
<h2>我喜欢的网站: {{mySite.name}}</h2>
<p>网站列表:</p>
<ul>
<li *ngFor="let site of sites">
{{ site.name }}
</li>
</ul>
<p *ngIf="sites.length > 3">你有很多个喜欢的网站!</p>
`
})
export class AppComponent {
title = '站点列表';
sites = [
new Site(1, '菜鸟教程'),
new Site(2, 'Google'),
new Site(3, 'Taobao'),
new Site(4, 'Facebook')
];
mySite = this.sites[0];
}
修改后,浏览器显示如下所示,底部多了个提示信息:
5 Angular 用户输入
用户点击链接、按下按钮或者输入文字时,这些用户的交互行为都会触发 DOM 事件。
本节将介绍如何使用 Angular 事件绑定语法来绑定这些事件。
5.1 绑定到用户输入事件
我们可以使用 Angular 事件绑定机制来响应任何 DOM 事件 。
以下实例将绑定了点击事件:
<button (click)="onClickMe()">点我!</button>
等号左边的 (click) 表示把该按钮的点击事件作为绑定目标 。 等号右边,引号中的文本是一个 模板语句。完整代码如下:
app/click-me.component.ts 文件:
import { Component } from '@angular/core';
@Component({
selector: 'click-me',
template: `
<button (click)="onClickMe()">点我!</button>
{{clickMessage}}`
})
export class ClickMeComponent {
clickMessage = '';
onClickMe() {
this.clickMessage = 'Angular教程!';
}
}
5.2 通过 $event 对象取得用户输入
我们可以绑定到所有类型的事件。
让我们试试绑定到一个输入框的 keyup 事件,并且把用户输入的东西回显到屏幕上。
app/keyup.component.ts (v1) 文件:
@Component({
selector: 'key-up1',
template: `
<input (keyup)="onKey($event)">
<p>{{values}}</p>
`
})
export class KeyUpComponent_v1 {
values = '';
/*
// 非强类型
onKey(event:any) {
this.values += event.target.value + ' | ';
}
*/
// 强类型
onKey(event: KeyboardEvent) {
this.values += (<HTMLInputElement>event.target).value + ' | ';
}
}
以上代码中我们监听了一个事件并捕获用户输入,Angular 把事件对象存入 $event 变量中。
组件的 onKey() 方法是用来从事件对象中提取出用户输入的,再将输入的值累加到 values 的属性。
5.3 从一个模板引用变量中获得用户输入
你可以通过使用局部模板变量来显示用户数据,模板引用变量通过在标识符前加上井号 (#) 来实现。
下面的实例演示如何使用局部模板变量:
app/loop-back.component.ts 文件:
@Component({
selector: 'loop-back',
template: `
<input #box (keyup)="0">
<p>{{box.value}}</p>
`
})
export class LoopbackComponent { }
我们在 <input>
元素上定义了一个名叫 box
的模板引用变量。 box
变量引用的就是 <input>
元素本身,这意味着我们可以获得 input 元素的 value
值,并通过插值表达式把它显示在 <p>
标签中。
我们可以使用模板引用变量来修改以上 keyup 的实例:
app/keyup.components.ts (v2) 文件:
@Component({
selector: 'key-up2',
template: `
<input #box (keyup)="onKey(box.value)">
<p>{{values}}</p>
`
})
export class KeyUpComponent_v2 {
values = '';
onKey(value: string) {
this.values += value + ' | ';
}
}
5.4 按键事件过滤 ( 通过 key.enter)
我们可以只在用户按下回车 (enter) 键的时候才获取输入框的值。
(keyup) 事件处理语句会监听到每一次按键,我们可以过滤按键,比如每一个 $event.keyCode,只有在按下回车键才更新 values 属性。
Angular 可以为我们过滤键盘事件,通过绑定到 Angular 的 keyup.enter 监听回车键的事件。
app/keyup.components.ts (v3):
@Component({
selector: 'key-up3',
template: `
<input #box (keyup.enter)="values=box.value">
<p>{{values}}</p>
`
})
export class KeyUpComponent_v3 {
values = '';
}
5.5 blur( 失去焦点 ) 事件
接下来我们可以使用blur( 失去焦点 ) 事件,它可以再元素失去焦点后更新 values 属性。
以下实例同时监听输入回车键与输入框失去焦点的事件。
app/keyup.components.ts (v4):
@Component({
selector: 'key-up4',
template: `
<input #box
(keyup.enter)="values=box.value"
(blur)="values=box.value">
<p>{{values}}</p>
`
})
export class KeyUpComponent_v4 {
values = '';
}
6 Angular 表单
本节将介绍如何使用组件和模板构建一个 Angular 表单。
利用 Angular 模板,我们可以创建各种类型表单,例如:登录表单,联系人表单,商品详情表单等,而且也为这些表单的字段添加数据校验。
接下来一步步来实现表单的功能。
6.1 创建项目
导入初始化项目。
完整的项目创建可以参考:Angular 2 TypeScript 环境配置
或者直接下载源代码:点我下载
解压后,修改目录名为angular-forms,修改 angular-forms/package.json 文件中的 "name": "angular-quickstart" 为 "name": "angular-forms"。
完成后,我们执行 cnpm install 来载入依赖包。
6.2 创建 Site 模型
以下创建了一个简单的模型类 Site,包含了三个必需字段:id,name,url,一个可选字段:alexa。
在 angular-forms/app 目录下创建 site.ts 文件,代码如下:
app/site.ts 文件:
export class Site {
constructor(
public id: number,
public name: string,
public url: string,
public alexa?: number
) { }
}
以上代码中,标为 public 的为公有字段,alexa 后添加一个问号(?)表示可选字段。
6.3 创建一个表单组件
每个 Angular 表单分为两部分:一个基于 HTML 的模板,和一个基于代码的组件,它用来处理数据和用户交互。
在 angular-forms/app 目录下创建 site-form.component.ts 文件,代码如下:
import { Component } from '@angular/core';
import { Site } from './site';
@Component({
moduleId: module.id,
selector: 'site-form',
templateUrl: 'site-form.component.html'
})
export class SiteFormComponent {
urls = ['www.runoob.com', 'www.google.com',
'www.taobao.com', 'www.facebook.com'];
model = new Site(1, '菜鸟教程', this.urls[0], 10000);
submitted = false;
onSubmit() { this.submitted = true; }
// TODO: 完成后移除
get diagnostic() { return JSON.stringify(this.model); }
}
实例中导入了 Component 装饰器和 Site 模型。
@Component 选择器 "site-form" 表示我们可以通过一个 <site-form> 标签,把此表单扔进父模板中。
templateUrl 属性指向一个独立的HTML模板文件,名叫 site-form.component.html。
diagnostic 属性用于返回这个模型的JSON形式。
6.4 定义应用的根模块
修改 app.module.ts 来定义应用的根模块,模块中指定了引用到的外部及声明属于本模块中的组件,比如 SiteFormComponent。
因为模板驱动的表单有它们自己的模块,所以我们得把 FormsModule 添加到本应用的 imports 数组中,这样我们才能使用表单。
app/app.module.ts 文件代码如下:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { SiteFormComponent } from './site-form.component';
@NgModule({
imports: [
BrowserModule,
FormsModule
],
declarations: [
AppComponent,
SiteFormComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
6.5 创建根组件
修改根组件文件 app.component.ts,将 SiteFormComponent 放在其中。
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: '<site-form></site-form>'
})
export class AppComponent { }
6.6 创建一个初始 HTML 表单模板
创建模板文件 site-form.component.html ,代码如下所示:
<div class="container">
<h1>网站表单</h1>
<form>
<div class="form-group">
<label for="name">网站名</label>
<input type="text" class="form-control" id="name" required>
</div>
<div class="form-group">
<label for="alexa">alexa 排名</label>
<input type="text" class="form-control" id="alexa">
</div>
<button type="submit" class="btn btn-default">提交</button>
</form>
</div
required 属性设置的该字段为必需字段,如果没有设置则是可选。
在 angular-forms 目录下输入以下命令:
cnpm install bootstrap --save
打开 index.html 文件,把以下样式链接添加到 <head> 中:
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
执行 npm start 后,访问:http://localhost:3000/,输出效果如下:
6.7 使用 ngModel 进行双向数据绑定
接下来我们使用 ngModel 进行双向数据绑定,通过监听 DOM 事件,来实现更新组件的属性。
修改 app/site-form.component.html ,使用 ngModel 把我们的表单绑定到模型。代码如下所示:
<div class="container">
<h1>网站表单</h1>
<form>
{{diagnostic}}
<div class="form-group">
<label for="name">网站名</label>
<input type="text" class="form-control" id="name"
required
[(ngModel)]="model.name" name="name">
</div>
<div class="form-group">
<label for="alexa">alexa 排名</label>
<input type="text" class="form-control" id="alexa"
[(ngModel)]="model.alexa" name="alexa">
</div>
<div class="form-group">
<label for="url">网站 URL </label>
<select class="form-control" id="url"
required
[(ngModel)]="model.url" name="url">
<option *ngFor="let p of urls" [value]="p">{{p}}</option>
</select>
</div>
<button type="submit" class="btn btn-default">提交</button>
</form>
</div>
-
每个 input 元素都有一个 id 属性,它被 label 元素的 for 属性用来把标签匹配到对应的 input 。
-
每个 input 元素都有一个 name 属性, Angular 的表单模块需要使用它为表单注册控制器。
运行以上实例输出结果如下:
{{diagnostic}} 只是用于测试时候输出数据使用。
我们还可以通过 ngModel 跟踪修改状态与有效性验证,它使用了三个 CSS 类来更新控件,以便反映当前状态。
状态 | 为 true 时的类 | 为 false 时的类 |
---|---|---|
控件已经被访问过 | ng-touched | ng-untouched |
控件值已经变化 | ng-dirty | ng-pristine |
控件值是有效的 | ng-valid | ng-invalid |
这样我们就可以添加自定义 CSS 来反应表单的状态。
在 angular-forms 目录下创建 forms.css 文件,代码如下:
.ng-valid[required], .ng-valid.required {
border-left: 5px solid #42A948; /* green */
}
.ng-invalid:not(form) {
border-left: 5px solid #a94442; /* red */
}
打开 index.html 文件,把以下样式链接添加到 <head> 中:
<link rel="stylesheet" href="forms.css">
修改 app/site-form.component.html ,代码如下所示:
<div class="container">
<h1>网站表单</h1>
<form>
{{diagnostic}}
<div class="form-group">
<label for="name">网站名</label>
<input type="text" class="form-control" id="name"
required
[(ngModel)]="model.name" name="name"
#name="ngModel" >
<div [hidden]="name.valid || name.pristine"
class="alert alert-danger">
网站名是必需的
</div>
</div>
<div class="form-group">
<label for="alexa">alexa 排名</label>
<input type="text" class="form-control" id="alexa"
[(ngModel)]="model.alexa" name="alexa">
</div>
<div class="form-group">
<label for="url">网站 URL </label>
<select class="form-control" id="url"
required
[(ngModel)]="model.url" name="url">
<option *ngFor="let p of urls" [value]="p">{{p}}</option>
</select>
</div>
<button type="submit" class="btn btn-default">提交</button>
</form>
</div>
模板中通过把 div 元素的 hidden 属性绑定到 name 控件的属性,我们就可以控制"name"字段错误信息的可见性了。
删除掉 name 字段的数据,显示结果如下所示:
6.8 添加一个网站
接下来我们创建一个用于添加网站的表单,在 app/site-form.component.html 添加一个按钮:
<button type="button" class="btn btn-default" (click)="newSite()">添加网站</button>
将以上按钮事件绑定到组件方法上:
active = true;
newSite() {
this.model = new Site(5, '', '');
this.active = false;
setTimeout(() => this.active = true, 0);
}
我们给组件添加一个 active 标记,把它初始化为 true 。当我们添加一个新的网站时,它把 active 标记设置为 false , 然后通过一个快速的 setTimeout 函数迅速把它设置回 true 。
6.9 通过 ngSubmit 来提交表单
我们可以使用 Angular 的指令 NgSubmit 来提交表单, 并且通过事件绑定机制把它绑定到 SiteFormComponent.submit() 方法上。
<form *ngIf="active" (ngSubmit)="onSubmit()" #siteForm="ngForm">
我们定义了一个模板引用变量 #siteForm ,并且把它初始化为 "ngForm" 。
这个 siteForm 变量现在引用的是 NgForm 指令,它代表的是表单的整体。
site-form.component.ts 文件完整代码如下:
import { Component } from '@angular/core';
import { Site } from './site';
@Component({
moduleId: module.id,
selector: 'site-form',
templateUrl: 'site-form.component.html'
})
export class SiteFormComponent {
urls = ['www.runoob.com', 'www.google.com',
'www.taobao.com', 'www.facebook.com'];
model = new Site(1, '菜鸟教程', this.urls[0], 10000);
submitted = false;
onSubmit() { this.submitted = true; }
// TODO: 完成后移除
get diagnostic() { return JSON.stringify(this.model); }
active = true;
newSite() {
this.model = new Site(5, '', '');
this.active = false;
setTimeout(() => this.active = true, 0);
}
}
app/site-form.component.html 完整代码如下:
<div class="container">
<div [hidden]="submitted">
<h1>网站表单</h1>
<form *ngIf="active" (ngSubmit)="onSubmit()" #siteForm="ngForm">
{{diagnostic}}
<div class="form-group">
<label for="name">网站名</label>
<input type="text" class="form-control" id="name"
required
[(ngModel)]="model.name" name="name"
#name="ngModel" >
<div [hidden]="name.valid || name.pristine"
class="alert alert-danger">
网站名是必需的
</div>
</div>
<div class="form-group">
<label for="alexa">alexa 排名</label>
<input type="text" class="form-control" id="alexa"
[(ngModel)]="model.alexa" name="alexa">
</div>
<div class="form-group">
<label for="url">网站 URL </label>
<select class="form-control" id="url"
required
[(ngModel)]="model.url" name="url">
<option *ngFor="let p of urls" [value]="p">{{p}}</option>
</select>
</div>
<button type="submit" class="btn btn-default" [disabled]="!siteForm.form.valid">提交</button>
<button type="button" class="btn btn-default" (click)="newSite()">新增网站</button>
</form>
</div>
<div [hidden]="!submitted">
<h2>你提交的信息如下:</h2>
<div class="row">
<div class="col-xs-3">网站名</div>
<div class="col-xs-9 pull-left">{{ model.name }}</div>
</div>
<div class="row">
<div class="col-xs-3">网站 alexa 排名</div>
<div class="col-xs-9 pull-left">{{ model.alexa }}</div>
</div>
<div class="row">
<div class="col-xs-3">网站 URL </div>
<div class="col-xs-9 pull-left">{{ model.url }}</div>
</div>
<br>
<button class="btn btn-default" (click)="submitted=false">编辑</button>
</div>
</div>
模板中我们把 hidden 通过属性绑定到 SiteFormComponent.submitted 属性上。
主表单从一开始就是可见的,因为 submitted 属性是 false ,当我们提交了这个表单则隐藏,submitted 属性是 true:
submitted = false;
onSubmit() { this.submitted = true; }
最终的目录结构为:
本文所使用的源码可以通过以下方式下载,不包含 node_modules 和 typings 目录。
6.10 完整实例演示 GIf 如下:
7 Angular 模板语法
前面几章节已经接触了 Angular 的模板,本文将具体介绍 Angular 的语法使用。
模板扮演的是一个视图的角色,简单讲就是展示给用户看的部分。
7.1 HTML
HTML 是 Angular 模板的"语言",除了 <script> 元素是被禁用的外 ,其他 HTML 元素都是支持的,例如:
<h1>我的第一个 Angular 应用</h1>
7.2 插值表达式
插值表达式的语法格式为:{{ ... }}。
插值表达式可以把计算的字符串插入HTML中,也可以作为属性值来使用。
<h3>
{{title}}
<img decoding="async" src="{{imageUrl}}" style="height:30px">
</h3>
7.3 模板表达式
{{ ... }} 里头其实就是一个模板表达式,Angular 会对其进行求值并转化为字符串输出。
以下实例是两个数相加:
<!-- "The sum of 1 + 1 is 2" -->
<p>The sum of 1 + 1 is {{1 + 1}}</p>
我们可以使用 getVal() 来获取这个表达式的值:
<div class="example">
<div class="example_code">
[mycode3 type="html"]
<!-- "4" -->
<p>{{1 + 1 + getVal()}}</p>
模板表达式类似 JavaScript 的语言,很多 JavaScript 表达式也是合法的模板表达式,但不是全部。
以下 JavaScript 表达式是禁止的:
- 赋值表达式(
=
,+=
,-=
...)- new操作符
- 带有
;
或者'
的连接表达式- 自增和自减操作(
++
和--
) 其他与Javascript语法不同的值得注意的包括:- 不支持位运算符(
|
和&
)- 模板表达式的操作符,如
|
和?.
等,被赋予了新的含义
7.4 属性绑定
模板的属性绑定可以把视图元素的属性设置为模板表达式 。
最常用的属性绑定是把元素的属性设置为组件中属性的值。 下面这个例子中, image 元素的 src 属性会被绑定到组件的 imageUrl 属性上:
<img [src]="imageUrl">
当组件为 isUnchanged( 未改变 ) 时禁用一个按钮:
<button [disabled]="isUnchanged">按钮是禁用的</button>
设置指令的属性:
<div [ngClass]="classes">[ngClass]绑定到classes 属性</div>
设置一个自定义组件的属性(这是父子组件间通讯的重要途径):
<user-detail [user]="currentUser"></user-detail>
7.5 HTML 属性(Attribute)、 class 和 style 绑定
模板语法为那些不太适合使用属性绑定的场景提供了专门的单向数据绑定形式。
7.5.1 属性(Attribute)、绑定
当元素没有属性可绑的时候,使用HTML标签属性(Attribute)绑定。
考虑 ARIA, SVG 和 table 中的 colspan/rowspan 等属性(Attribute) 。它们是纯粹的属性 。 它们没有对应的属性可供绑定。
以下实例会报错:
<tr><td colspan="{{1 + 1}}">Three-Four</td></tr>
我们会得到这个错误:
Template parse errors:
Can't bind to 'colspan' since it isn't a known native property
模板解析错误:不能绑定到'colspan',因为它不是已知的原生属性
正如提示中所说, <td>
元素没有 colspan
属性。 但是插值表达式和属性绑定只能设置 属性 ,而不是 Attribute,所以需HTML标签 Attribute 绑定来创建和绑定类似的Attribute。
HTML标签特性绑定在语法上类似于属性绑定,但中括号中的部分不是一个元素的属性名,而是由一个attr.的前缀和HTML标签属性的名称组成,然后通过一个能求值为字符串的表达式来设置HTML标签属性的值。如:
<table border=1>
<tr><td [attr.colspan]="1 + 1">One-Two</td></tr>
<tr><td>Five</td><td>Six</td></tr>
</table>
7.5.2 css类绑定
借助 CSS 类绑定 ,我们可以从元素的 class 属性上添加和移除 CSS 类名。
CSS 类绑定在语法上类似于属性绑定。但方括号中的部分不是一个元素的属性名,而是包括一个 class 前缀,紧跟着一个点 (.) ,再跟着 CSS 类的名字组成。 其中后两部分是可选的。例如: [class.class-name] 。
下面的例子展示了如何通过css类绑定类添加和移除"special"类:
<!-- 标准HTML样式类设置 -->
<div class="bad curly special">Bad curly special</div>
<!-- 通过绑定重设或覆盖样式类 -->
<div class="bad curly special" [class]="badCurly">Bad curly</div>
<!-- 通过一个属性值来添加或移除special样式类 -->
<div [class.special]="isSpecial">这个样式比较特殊</div>
7.5.3 style样式绑定
通过样式绑定,可以设置内联样式。样式绑定语法上类似于属性绑定,但中括号里面的部分不是一个元素的属性名,样式绑定包括一个style.,紧跟着css样式的属性名,例如:[style.style-property]。
<button [style.color] = "isSpecial ? 'red': 'green'">红色</button>
<button [style.background-color]="canSave ? 'cyan': 'grey'" >保存</button>
<!-- 带有单位的样式绑定 -->
<button [style.font-size.em]="isSpecial ? 3 : 1" >大</button>
<button [style.font-size.%]="!isSpecial ? 150 : 50" >小</button>
7.6 事件绑定
在事件绑定中,Angular通过监听用户动作,比如键盘事件、鼠标事件、触屏事件等来响应相对应的数据流向-从视图目标到数据源。
事件绑定的语法是由等号左侧小括号内的 目标事件 和右侧引号中的 模板声明 组成。
比如下面这个例子,是事件绑定监听按钮的点击事件。只要点击鼠标,都会调用组件的 onSave()方法。
<button (click)="onSave()">保存</button>
圆括号中的名称 ——比如 (click) ——标记出了目标事件。在下面这个例子中,目标是按钮的 click 事件。
<button (click)="onSave()">Save</button>
也可以使用on- 前缀的形式:
<button on-click="onSave()">On Save</button>
7.7 使用 NgModel 进行双向数据绑定
当开发数据输入表单时,期望的结果是既能将组件的数据显示到表单上,也能在用户修改时更新组件的数据。
以下是一个通过 [(NgModel)] 来实现双向绑定:
<input [(ngModel)]="currentUser.firstName">
[]实现了数据流从组件到模板,()实现了数据流从模板到组件,两者一结合[()]就实现了双向绑定。
使用前缀形式的语法:
<input bindon-ngModel="currentUser.firstName">
7.8 Angular 内置指令
Angular 的内置指令有 NgClass、NgStyle、NgIf、NgFor、NgSwitch等。
7.8.1 NgClass
通过绑定到 NgClass 动态添加或删除 CSS 类。
<div [style.font-size]="isSpecial ? 'x-large' : 'smaller'" >
这个div是大号的。
</div>
7.8.2 NgStyle
NgStyle 通过把它绑定到一个key:value控制对象的形式,可以让我们同时设置很多内联样式。
setStyles() {
let styles = {
// CSS属性名
'font-style': this.canSave ? 'italic' : 'normal', // italic
'font-weight': !this.isUnchanged ? 'bold' : 'normal', // normal
'font-size': this.isSpecial ? '24px' : '8px', // 24px
};
return styles;
}
通过添加一个NgStyle属性绑定,让它调用setStyles,并据此来设置元素的样式:
<div [ngStyle]="setStyles()">
这个div的样式是italic, normal weight, 和extra large (24px)。
</div>
7.8.3 NgIf
通过把NgIf指令绑定到一个真值表达式,可以把一个元素及其子元素添加到DOM树上。
<div *ngIf="currentUser">Hello,{{currentUser.firstName}}</div>
相反,绑定到一个假值表达式将从DOM树中移除该元素及其子元素。如:
<!-- 因为isActive的值为false,所以User Detail不在DOM树中-->
<user-detail *ngIf="isActive"></user-detail>
7.8.4 NgSwitch
当需要从一组可能的元素树中根据条件显示其中一个时,就需要NgSwitch了。Angular将只把选中的元素添加进DOM中。如:
<span [ngSwitch]="userName">
<span *ngSwitchCase="'张三'">张三</span>
<span *ngSwitchCase="'李四'">李四</span>
<span *ngSwitchCase="'王五'">王五</span>
<span *ngSwitchCase="'赵六'">赵六</span>
<span *ngSwitchDefault>龙大</span>
</span>
把作为父指令的NgSwitch
绑定到一个能返回开关值的表达式,例子中这个值是字符串,但它可以是任何类型的值。父指令NgSwitch
控制一组<span>
子元素。每个<span>
或者挂在一个匹配值的表达式上,或者被标记为默认情况。任何时候,这些span中最多只有一个会出现在DOM中。如果这个span的匹配值和开关值相等,Angular2就把这个<span>
添加DOM中。如果没有任何span匹配上,Angular2就会把默认的span添加到DOM中。Angular2会移除并销毁所有其他的span。
三个相互合作的指令:
- ngSwitch:绑定到一个返回开关值的表达式
- ngSwitchCase:绑定到一个返回匹配值的表达式
- ngSwitchDefault:一个用于标记默认元素的属性
Note :不要再ngSwitch前使用
*
,而应该用属性绑定,但ngSwitchCase和ngSwitchDefault前面要放*
。
7.8.5 NgFor
当需要展示一个由多个条目组成的列表时就需要这个指令了。如下面这个例子,就是在一个HTML块上应用NgFor。
<div *ngFor="let user of users">{{user.fullName}}</div>
NgFor也可以应用在一个组件元素上,如:
<user-detail *ngFor="let user of users" [user]="user"></user-detail>
ngFor指令支持一个可选的index索引,在迭代过程中会从0增长到数组中的长度。
可以通过模板输入变量来捕获这个index,并应用在模板中。下面的例子就把index捕获到了一个名为i的变量中。
<div *ngFor="let user of users; let i=index">{{i + 1}} - {{user.fullName}}</div>
7.8.6 NgForTrackBy
ngFor 指令有时候会性能较差,特别是在大型列表中。 对一个条目的一点小更改、移除或添加,都会导致级联的 DOM 操作。
比如,当通过重新从服务器来刷新通讯录,刷新后的列表可能包含很多(如果不是全部的话)以前显示过的联系人。但在Angular看来,它不知道哪些是以前就存在过的,只能清理旧列表、舍弃那些DOM元素,并用新的DOM元素来重建一个新列表。
解决这个问题,可以通过追踪函数来避免这种折腾。追踪函数会告诉Angular:我们知道两个具有相同user.id的对象是同一个联系人。如:
trackByUsers(index: number, user: User){return user.id}
然后,把NgForTrackBy指令设置为那个追踪函数:
<div *ngFor="let user of users; trackBy:trackByUsers">
({{user.id}}) {{user.fullName}}
</div>
追踪函数不会排除所有DOM更改。如果用来判断是否为同一个联系人的属性变化了,就会更新DOM元素,反之就会留下这个DOM元素。列表界面就会变得比较更加平滑,具有更好的响应效果。
8 模板引用变量
模板引用变量是模板中对 DOM 元素或指令的引用。
它能在原生 DOM 元素中使用,也能用于 Angular 组件——实际上,它可以和任何自定义 Web 组件协同工作。
我们可以在同一元素、兄弟元素或任何子元素中引用模板引用变量。
这里是关于创建和使用模板引用变量的两个例子:
<!-- phone 引用了 input 元素,并将 `value` 传递给事件句柄 -->
<input #phone placeholder="phone number">
<button (click)="callPhone(phone.value)">Call</button>
<!-- fax 引用了 input 元素,并将 `value` 传递给事件句柄 -->
<input ref-fax placeholder="fax number">
<button (click)="callFax(fax.value)">Fax</button>
"phone" 的 (#) 前缀意味着我们将要定义一个 phone 变量。