Angular快速学习笔记(3) -- 组件与模板

1. 显示数据

在 Angular 中最典型的数据显示方式,就是把 HTML 模板中的控件绑定到 Angular 组件的属性。

使用插值表达式显示组件属性

要显示组件的属性,最简单的方式就是通过插值表达式 (interpolation) 来绑定属性名。 要使用插值表达式,就把属性名包裹在双花括号里放进视图模板,如 {{myHero}}。

import { Component } from '@angular/core';
 
@Component({
  selector: 'app-root',
  template: `
    <h1>{{title}}</h1>
    <h2>My favorite hero is: {{myHero}}</h2>
    `
})
export class AppComponent {
  title = 'Tour of Heroes';
  myHero = 'Windstorm';
}

注意

  • 这里直接使用的 template,而不是templateUrl,template可以直接书写html代码,简单的html推荐这种方式
  • @Component 装饰器中指定的 CSS 选择器 selector,它指定了一个叫 my-app 的元素。 该元素是 index.html 的 body 里的占位符。
#src/index.html (body)
<body>
  <app-root></app-root>
</body>

内联 (inline) 模板还是模板文件?

angular提供两种地方存放组件模板

  • 你可以使用 template 属性把它定义为内联的,
  • 或者把模板定义在一个独立的 HTML 文件中, 再通过 @Component 装饰器中的 templateUrl 属性, 在组件元数据中把它链接到组件

到底选择内联 HTML 还是独立 HTML 取决于个人喜好、具体状况和组织级策略。 上面的应用选择内联 HTML ,是因为模板很小,而且没有额外的 HTML 文件显得这个演示简单些。

为数据创建一个类

使用angular提供的cli:

ng generate class hero

修改src/app/hero.ts

export class Hero {
  constructor(
    public id: number,
    public name: string) { }
}

使用这个类:

src/app/app.component.ts (heroes):

heroes = [
  new Hero(1, 'Windstorm'),
  new Hero(13, 'Bombasto'),
  new Hero(15, 'Magneta'),
  new Hero(20, 'Tornado')
];
myHero = this.heroes[0];
template: `
  <h1>{{title}}</h1>
  <h2>My favorite hero is: {{myHero.name}}</h2>
  <p>Heroes:</p>
  <ul>
    <li *ngFor="let hero of heroes">
      {{ hero.name }}
    </li>
  </ul>
`

在模板里可以自己使用.语法,访问对象属性

使用 ngFor 显示数组属性

*ngFor 是 Angular 的“迭代”指令。 它将 <li> 元素及其子级标记为“迭代模板”.,同vue.js里的v-for


export class AppComponent {
  title = 'Tour of Heroes';
  heroes = ['Windstorm', 'Bombasto', 'Magneta', 'Tornado'];
  myHero = this.heroes[0];
}

template: `
  <h1>{{title}}</h1>
  <h2>My favorite hero is: {{myHero}}</h2>
  <p>Heroes:</p>
  <ul>
    <li *ngFor="let hero of heroes">
      {{ hero }}
    </li>
  </ul>
  ·

不要忘记 *ngFor 中的前导星号 (*)。它是语法中不可或缺的一部分

通过 NgIf 进行条件显示

有时,应用需要只在特定情况下显示视图或视图的一部分,这个时候使用ngif,同vue.js里的v-if

<p *ngIf="heroes.length > 3">There are many heroes!</p>

小结

  • 带有双花括号的插值表达式 (interpolation) 来显示一个组件属性
  • 用 ngFor 显示数组
  • 用一个 TypeScript 类来为你的组件描述模型数据并显示模型的属性
  • 用 ngIf 根据一个布尔表达式有条件地显示一段 HTML。

2. 模板语法

Angular 应用管理着用户之所见和所为,并通过 Component 类的实例(组件)和面向用户的模板来与用户交互。

在 Angular 中,组件扮演着控制器视图模型的角色,模板则扮演视图的角色。

### 模板中的 HTML

HTML 是 Angular 模板的语言。几乎所有的 HTML 语法都是有效的模板语法。 但值得注意的例外是 <script>元素,它被禁用了,以阻止脚本注入攻击的风险。(实际上,<script> 只是被忽略了。)

### 插值表达式 ( {{...}})

插值表达式{{...}}可以把计算后的字符串插入到 HTML 元素标签内的文本或对标签的属性进行赋值。

<h3>
  {{title}}
  <img src="{{heroImageUrl}}" style="height:30px">
</h3>

一般来说,括号间的素材是一个模板表达式,Angular 先对它求值,再把它转换成字符串。 下列插值表达式通过把括号中的两个数字相加说明了这一点:

<p>The sum of 1 + 1 is {{1 + 1}}</p>

表达式还可以调用宿主组件的方法,就像下面用的 getVal():

<p>The sum of 1 + 1 is not {{1 + 1 + getVal()}}</p>

模板表达式

模板表达式产生一个值。 Angular 执行这个表达式,并把它赋值给绑定目标的属性,这个绑定目标可能是 HTML 元素、组件或指令。

典型的表达式上下文就是这个组件实例,它是各种绑定值的来源。 在下面的代码片段中,双花括号中的 title 和引号中的 isUnchanged 所引用的都是 AppComponent 中的属性。

{{title}}
<span [hidden]="isUnchanged">changed</span>

表达式中的上下文变量是由模板变量、指令的上下文变量(如果有)和组件的成员叠加而成的。 如果你要引用的变量名存在于一个以上的命名空间中,那么,模板变量是最优先的,其次是指令的上下文变量,最后是组件的成员。

<div *ngFor="let hero of heroes">{{hero.name}}</div>
<input #heroInput> {{heroInput.value}}

模板语句

模板语句用来响应由绑定目标(如 HTML 元素、组件或指令)触发的事件。 它出现在 = 号右侧的引号中,就像这样:(event)="statement"。

<button (click)="deleteHero()">Delete hero</button>

和模板表达式一样,模板语句使用的语言也像 JavaScript。 模板语句解析器和模板表达式解析器有所不同,特别之处在于它支持基本赋值 (=) 和表达式链 (; 和 ,)

某些 JavaScript 语法仍然是不允许的:

  • new 运算符
  • 自增和自减运算符:++ 和 --
  • 操作并赋值,例如 += 和 -=
  • 位操作符 | 和 &
  • 模板表达式运算符

和表达式中一样,语句只能引用语句上下文中 —— 通常是正在绑定事件的那个组件实例。

典型的语句上下文就是当前组件的实例。 (click)="deleteHero()" 中的 deleteHero 就是这个数据绑定组件上的一个方法。

模板语句不能引用全局命名空间的任何东西。比如不能引用 window 或 document,也不能调用 console.log 或 Math.max。

绑定语法

数据绑定是一种机制,用来协调用户所见和应用数据。绑定的类型可以根据数据流的方向分成三类: 从数据源到视图、从视图到数据源以及双向的从视图到数据源再到视图。

数据方向 语法 绑定类型
单向从数据源到视图 {{expression}} [target]="expression" bind-target="expression" 插值表达式\属性\Attribute\CSS 类\样式
从视图到数据源的单向绑定 (target)="statement"、on-target="statement" 事件
双向 [(target)]="expression" 、bindon-target="expression" 双向

新的思维模型

数据绑定的威力和允许用自定义标记扩展 HTML 词汇的能力,会让你把模板 HTML 当成 HTML+。

在正常的 HTML 开发过程中,你使用 HTML 元素来创建视觉结构, 通过把字符串常量设置到元素的 attribute 来修改那些元素。

<div class="special">Mental Model</div>
<img src="assets/images/hero.png">
<button disabled>Save</button>

在 Angular 模板中,你仍使用同样的方式创建结构和初始化 attribute 值。然后,用封装了 HTML 的组件创建新元素,并把它们当作原生 HTML 元素在模板中使用。

<!-- Normal HTML -->
<div class="special">Mental Model</div>
<!-- Wow! A new element! -->
<app-hero-detail></app-hero-detail>

改变定式思维;

<button [disabled]="isUnchanged">Save</button>

直觉告诉你,你正在绑定按钮的 disabled attribute。 并把它设置为组件的 isUnchanged 属性的当前值,但你的直觉是错的!isUnchanged为true时,button增加disabled属性。

模板绑定是通过 property 和事件来工作的,而不是 attribute.

数据绑定的目标是 DOM 中的某些东西。 这个目标可能是(元素 | 组件 | 指令的)property、(元素 | 组件 | 指令的)事件,或(极少数情况下) attribute 名。

属性绑定
<img [src]="heroImageUrl">
<app-hero-detail [hero]="currentHero"></app-hero-detail>
<div [ngClass]="{'special': isSpecial}"></div>

注意需要加[]方括号。

下面的代码是等价的:

<p><img src="{{heroImageUrl}}"> is the <i>interpolated</i> image.</p>
<p><img [src]="heroImageUrl"> is the <i>property bound</i> image.</p>

在多数情况下,插值表达式是更方便的备选项。 实际上,在渲染视图之前,Angular 把这些插值表达式翻译成相应的属性绑定。

事件绑定

可以通过 Angular 事件绑定来声明对哪些用户动作感兴趣

圆括号中的名称 —— 比如 (click) —— 标记出目标事件。在下面例子中,目标是按钮的 click 事件。

<button (click)="onSave()">Save</button>
<app-hero-detail (deleteRequest)="deleteHero()"></app-hero-detail>
<div (myClick)="clicked=$event" clickable>click me</div>

也可以使用on-前缀方式:

<button on-click="onSave()">On Save</button>
$event 对象

在事件绑定中,Angular 会为目标事件设置事件处理器。绑定会通过名叫 $event 的事件对象传递关于此事件的信息。

<input [value]="currentHero.name"
       (input)="currentHero.name=$event.target.value" >
使用 EventEmitter 实现自定义事件

通常,指令使用 Angular EventEmitter 来触发自定义事件。 指令创建一个 EventEmitter 实例,并且把它作为属性暴露出来。 指令调用 EventEmitter.emit(payload) 来触发事件,可以传入任何东西作为消息载荷。 父指令通过绑定到这个属性来监听事件,并通过 $event 对象来访问载荷。


template: `
<div>
  <img src="{{heroImageUrl}}">
  <span [style.text-decoration]="lineThrough">
    {{prefix}} {{hero?.name}}
  </span>
  <button (click)="delete()">Delete</button>
</div>`

deleteRequest = new EventEmitter<Hero>();

delete() {
  this.deleteRequest.emit(this.hero);
}

使用自定义事件:

<app-hero-detail (deleteRequest)="deleteHero($event)" [hero]="currentHero"></app-hero-detail>
双向绑定

对于需要显示数据属性,并在用户作出更改时更新该属性如何处理呢?
在元素层面上,既要设置元素属性,又要监听元素事件变化。Angular 为此提供一种特殊的双向数据绑定语法:[(x)][(x)] 语法结合了属性绑定的方括号[x] 和事件绑定的圆括号(x)

<input [(ngModel)]="name">
Attribute

attribute 绑定的语法与属性绑定类似。 但方括号中的部分不是元素的属性名,而是由attr前缀,一个点 (.) 和 attribute 的名字组成。

<button [attr.aria-label]="help">help</button>
CSS 类

借助 CSS 类绑定,可以从元素的 class attribute 上添加和移除 CSS 类名。

<div [class.special]="isSpecial">Special</div>
样式

样式绑定的语法与属性绑定类似。 但方括号中的部分不是元素的属性名,而由style前缀,一个点 (.)和 CSS 样式的属性名组成。 形如:[style.style-property]。

<button [style.color]="isSpecial ? 'red' : 'green'">

模板引用变量 ( #var )

模板引用变量通常用来引用模板中的某个 DOM 元素,它还可以引用 Angular 组件或指令或Web Component。
使用井号 (#) 来声明引用变量。#phone 的意思就是声明一个名叫 phone 的变量来引用<input> 元素。

<input #phone placeholder="phone number">

<!-- phone refers to the input element; pass its `value` to an event handler -->
<button (click)="callPhone(phone.value)">Call</button>

输入和输出属性

输入属性是一个带有 @Input装饰器的可设置属性。当它通过属性绑定的形式被绑定时,值会“流入”这个属性。

输出属性是一个带有 @Output 装饰器的可观察对象型的属性。 这个属性几乎总是返回 Angular 的EventEmitter。 当它通过事件绑定的形式被绑定时,值会“流出”这个属性。

你只能通过它的输入和输出属性将其绑定到其它组件。

模板表达式操作符

模板表达式语言使用了 JavaScript 语法的子集,并补充了几个用于特定场景的特殊操作符。 下面介绍其中的两个:管道和安全导航操作符

管道操作符 ( | )

在绑定之前,表达式的结果可能需要一些转换。例如,可能希望把数字显示成金额、强制文本变成大写,或者过滤列表以及进行排序。

Angular 管道对像这样的小型转换来说是个明智的选择。 管道是一个简单的函数,它接受一个输入值,并返回转换结果。 它们很容易用于模板表达式中,只要使用管道操作符 (|) 就行了。

<div>Title through uppercase pipe: {{title | uppercase}}</div>

安全导航操作符 ( ?. ) 和空属性路径

Angular 的安全导航操作符 (?.) 是一种流畅而便利的方式,用来保护出现在属性路径中 null 和 undefined 值。 下例中,当 currentHero 为空时,保护视图渲染器,让它免于失败。

The current hero's name is {{currentHero?.name}}

当绑定中 title 属性为空,仍然会继续渲染

非空断言操作符(!)

在 TypeScript 2.0 中,你可以使用 --strictNullChecks 标志强制开启严格空值检查。TypeScript 就会确保不存在意料之外的 null 或 undefined。
在这种模式下,有类型的变量默认是不允许 null 或 undefined 值的,如果有未赋值的变量,或者试图把 null 或 undefined 赋值给不允许为空的变量,类型检查器就会抛出一个错误

Angular 模板中的**非空断言操作符(!)也是同样的用途。

<div *ngIf="hero">
  The hero's name is {{hero!.name}}
</div>

与安全导航操作符不同的是,非空断言操作符不会防止出现 null 或 undefined。 它只是告诉 TypeScript 的类型检查器对特定的属性表达式,不做 "严格空值检测"。

3. angular 声明周期钩子

每个组件都有一个被 Angular 管理的生命周期,Angular 提供了生命周期钩子,把这些关键生命时刻暴露出来,你可以做一些自定义操作。

钩子 用途及时机
ngOnChanges() 当 Angular(重新)设置数据绑定输入属性时响应。 该方法接受当前和上一属性值的 [SimpleChanges](https://angular.cn/api/core/SimpleChanges) 对象当被绑定的输入属性的值发生变化时调用,首次调用一定会发生在 ngOnInit() 之前。
ngOnInit() 在 Angular 第一次显示数据绑定和设置指令/组件的输入属性之后,初始化指令/组件。Initialize the directive/component after Angular first displays the data-bound properties and sets the directive/component's input properties.在第一轮 ngOnChanges() 完成之后调用,只调用一次
ngDoCheck() 检测,并在发生 Angular 无法或不愿意自己检测的变化时作出反应。在每个 Angular 变更检测周期中调用,ngOnChanges() 和 ngOnInit() 之后。
ngAfterContentInit() 当把内容投影进组件之后调用。第一次 ngDoCheck() 之后调用,只调用一次。
ngAfterContentChecked() 每次完成被投影组件内容的变更检测之后调用。ngAfterContentInit() 和每次 ngDoCheck() 之后调用
ngAfterViewInit() 初始化完组件视图及其子视图之后调用。第一次 ngAfterContentChecked() 之后调用,只调用一次。
ngAfterViewChecked() 每次做完组件视图和子视图的变更检测之后调用。ngAfterViewInit() 和每次 ngAfterContentChecked() 之后调用。
ngOnDestroy() 当 Angular 每次销毁指令/组件之前调用并清扫。 在这儿反订阅可观察对象和分离事件处理器,以防内存泄漏。在 Angular 销毁指令/组件之前调用

OnInit钩子

使用 ngOnInit() 有两个原因:

  • 在构造函数之后马上执行复杂的初始化逻辑
  • 在 Angular 设置完输入属性之后,对该组件进行准备。

OnDestroy()钩子

一些清理逻辑必须在 Angular 销毁指令之前运行,把它们放在 ngOnDestroy() 中。

OnChanges() 钩子

一旦检测到该组件(或指令)的输入属性发生了变化,Angular 就会调用它的 ngOnChanges() 方法

ngOnChanges(changes: SimpleChanges) {
  for (let propName in changes) {
    let chng = changes[propName];
    let cur  = JSON.stringify(chng.currentValue);
    let prev = JSON.stringify(chng.previousValue);
    this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`);
  }
}

4.组件之间的交互

通过输入型绑定把数据从父组件传到子组件

HeroChildComponent 有两个输入型属性,它们通常带@Input 装饰器。

import { Component, Input } from '@angular/core';
 
import { Hero } from './hero';
 
@Component({
  selector: 'app-hero-child',
  template: `
    <h3>{{hero.name}} says:</h3>
    <p>I, {{hero.name}}, am at your service, {{masterName}}.</p>
  `
})
export class HeroChildComponent {
  @Input() hero: Hero;
  @Input('master') masterName: string;
}

父组件 HeroParentComponent。

import { Component } from '@angular/core';
 
import { HEROES } from './hero';
 
@Component({
  selector: 'app-hero-parent',
  template: `
    <h2>{{master}} controls {{heroes.length}} heroes</h2>
    <app-hero-child *ngFor="let hero of heroes"
      [hero]="hero"
      [master]="master">
    </app-hero-child>
  `
})
export class HeroParentComponent {
  heroes = HEROES;
  master = 'Master';
}

通过 setter 截听输入属性值的变化

如果要对父组件赋值做修改,可以使用setter

import { Component, Input } from '@angular/core';
 
@Component({
  selector: 'app-name-child',
  template: '<h3>"{{name}}"</h3>'
})
export class NameChildComponent {
  private _name = '';
 
  @Input()
  set name(name: string) {
    this._name = (name && name.trim()) || '<no name set>';
  }
 
  get name(): string { return this._name; }
}

通过ngOnChanges()来截听输入属性值的变化

使用 OnChanges 生命周期钩子接口的 ngOnChanges() 方法来监测输入属性值的变化并做出回应

下面的 VersionChildComponent 会监测输入属性 major 和 minor 的变化,并把这些变化编写成日志以报告这些变化

import { Component, Input, OnChanges, SimpleChange } from '@angular/core';
 
@Component({
  selector: 'app-version-child',
  template: `
    <h3>Version {{major}}.{{minor}}</h3>
    <h4>Change log:</h4>
    <ul>
      <li *ngFor="let change of changeLog">{{change}}</li>
    </ul>
  `
})
export class VersionChildComponent implements OnChanges {
  @Input() major: number;
  @Input() minor: number;
  changeLog: string[] = [];
 
  ngOnChanges(changes: {[propKey: string]: SimpleChange}) {
    let log: string[] = [];
    for (let propName in changes) {
      let changedProp = changes[propName];
      let to = JSON.stringify(changedProp.currentValue);
      if (changedProp.isFirstChange()) {
        log.push(`Initial value of ${propName} set to ${to}`);
      } else {
        let from = JSON.stringify(changedProp.previousValue);
        log.push(`${propName} changed from ${from} to ${to}`);
      }
    }
    this.changeLog.push(log.join(', '));
  }
}

父组件监听子组件的事件

子组件暴露一个 EventEmitter 属性,当事件发生时,子组件利用该属性 emits(向上弹射)事件。

子组件的 EventEmitter 属性是一个输出属性,通常带有@Output 装饰器

import { Component, EventEmitter, Input, Output } from '@angular/core';
 
@Component({
  selector: 'app-voter',
  template: `
    <h4>{{name}}</h4>
    <button (click)="vote(true)"  [disabled]="voted">Agree</button>
    <button (click)="vote(false)" [disabled]="voted">Disagree</button>
  `
})
export class VoterComponent {
  @Input()  name: string;
  @Output() onVoted = new EventEmitter<boolean>();
  voted = false;
 
  vote(agreed: boolean) {
    this.onVoted.emit(agreed);
    this.voted = true;
  }
}

父组件,可以绑定onVoted事件,框架(Angular)把事件参数(用 $event 表示)传给事件处理方法:

import { Component }      from '@angular/core';
 
@Component({
  selector: 'app-vote-taker',
  template: `
    <h2>Should mankind colonize the Universe?</h2>
    <h3>Agree: {{agreed}}, Disagree: {{disagreed}}</h3>
    <app-voter *ngFor="let voter of voters"
      [name]="voter"
      (onVoted)="onVoted($event)">
    </app-voter>
  `
})
export class VoteTakerComponent {
  agreed = 0;
  disagreed = 0;
  voters = ['Mr. IQ', 'Ms. Universe', 'Bombasto'];
 
  onVoted(agreed: boolean) {
    agreed ? this.agreed++ : this.disagreed++;
  }
}

父组件和子组件通过服务来通讯

在之前讲服务时就提过,同一个module下的组件间,可以通过服务进行通讯。父组件和它的子组件共享同一个服务,利用该服务在家庭内部实现双向通讯。

5.组件样式

Angular 应用使用标准的 CSS 来设置样式。这意味着你可以把关于 CSS 的那些知识和技能直接用于 Angular 程序中,例如:样式表、选择器、规则以及媒体查询等。

有几种方式把样式加入组件:

  • 设置 styles 或 styleUrls 元数据
  • 内联在模板的 HTML 中
  • 通过 CSS 文件导入

预编译css

如果使用 CLI 进行构建,那么你可以用 sass、less 或 stylus 来编写样式,并使用相应的扩展名(.scss、.less、.styl)把它们指定到 @Component.styleUrls 元数据中。例子如下:

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})

6.属性指令

在 Angular 中有三种类型的指令:

  1. 组件 — 拥有模板的指令
  2. 结构型指令 — 通过添加和移除 DOM 元素改变 DOM 布局的指令
  3. 属性型指令 — 改变元素、组件或其它指令的外观和行为的指令。

创建属性指令:

ng generate directive highlight

编写代码,这个指令实现高亮元素:

import { Directive, ElementRef } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
    constructor(el: ElementRef) {
       el.nativeElement.style.backgroundColor = 'yellow';
    }
}

通过ElementRef访问目标元素。

使用属性型指令

<p appHighlight>Highlight me!</p>

使用 @Input 数据绑定向指令传递值

@Input() highlightColor: string;

<p appHighlight highlightColor="yellow">Highlighted in yellow</p>
<p appHighlight [highlightColor]="'orange'">Highlighted in orange</p>

绑定到 @Input 别名

可以使用Input的别名

@Input('appHighlight') highlightColor: string;
<p [appHighlight]="color">Highlight me!</p>

作者:Jadepeng
出处:jqpeng的技术记事本--http://www.cnblogs.com/xiaoqi
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

posted @ 2018-05-21 16:31 JadePeng 阅读(...) 评论(...) 编辑 收藏