Angular4 - 指令

1. 指令分类

组件(Component directive):用于构建UI组件,继承于 Directive 类
属性指令(Attribute directive): 用于改变组件的外观或行为
结构指令(Structural directive): 用于动态添加或删除DOM元素来改变DOM布局

组件相关的内容就不多说了,可以查看文章组件


(1) 内置属性指令

属性型指令会监听和修改其它HTML元素或组件的行为、元素属性(Attribute)、DOM属性(Property)。 它们通常会作为HTML属性的名称而应用在元素上。此处介绍几个常用的指令,也是在前面模板语法中提及到的几个指令。


a) ngClass

我们经常用动态添加或删除 CSS 类的方式来控制元素如何显示。 通过绑定到NgClass,可以同时添加或移除多个类。
CSS 类绑定是添加或删除单个类的最佳途径。前文也有提到过。
<div [class.special]="isSpecial">The class binding is special</div>
对于多个class文件,NgClass是更好的选择。
绑定常量
<div [ngClass]="{'text-success': true }"></div>
绑定表达式
<div [ngClass]="{'text-success': person.country === 'CN'}"></div>
绑定多个class
<div [ngClass]="{'text-success': person.country === 'CN', 'color': true}"></div>


b) NgStyle

我们可以根据组件的状态动态设置内联样式。 NgStyle绑定可以同时设置多个内联样式。
样式绑定(http://blog.csdn.net/it_rod/article/details/79429457#t18)是设置单一样式值的简单方式。
如果要同时设置多个内联样式,NgStyle指令可能是更好的选择。
绑定常量
<div [ngStyle]="{'background-color': 'green'}"></div>
绑定表达式
<div [ngStyle]="{'background-color': person.country === 'UK' ? 'green' : 'red'}">
绑定多个style样式
<div [ngStyle]="{'background-color': person.country === 'UK' ? 'green' : 'red', 'color': red}">


(2) 内置结构性指令 

结构型指令的职责是HTML布局。 它们塑造或重塑DOM的结构,这通常是通过添加、移除和操纵它们所附加到的宿主元素来实现的。先介绍几个指令。

星号是一个用来简化更复杂语法的“语法糖”。 从内部实现来说,Angular把*ngIf 属性 翻译成一个<ng-template> 元素 并用它来包裹宿主元素。


a) NgIf 指令

通过把NgIf指令应用到元素上(称为宿主元素),我们可以往DOM中添加或从DOM中移除这个元素。 在下面的例子中,该指令绑定到了类似于isActive这样的条件表达式。
<div *ngIf="person.country === 'CN'">{{ person.name }} ({{person.country}})</div>
我们也可以通过css绑定来实现元素的隐藏,但是这个和NgIf排除元素是不同的,因为隐藏元素的时候,元素仍然在存在DOM中,但是NgIf是会将元素从DOM树种排除。

防范空指针错误
ngIf指令通常会用来防范空指针错误。 而显示/隐藏的方式是无法防范的,当一个表达式尝试访问空值的属性时,Angular就会抛出一个异常。
<div *ngIf="currentHero">Hello, {{currentHero.name}}</div>
这个例子只有当currentHero不为null/undefined才会显示,如果没有这个指令,那么这个地方可能会报错。

b) NgFor 指令

NgFor是一个重复器指令 —— 自定义数据显示的一种方式。 我们的目标是展示一个由多个条目组成的列表。首先定义了一个 HTML 块,它规定了单个条目应该如何显示。 再告诉 Angular 把这个块当做模板,渲染列表中的每个条目。
<div *ngFor="let hero of heroes">{{hero.name}}</div>

NgFor 微语法

赋值给*ngFor的字符串不是模板表达式。 它是一个微语法 —— 由 Angular 自己解释的小型语言。在这个例子中,字符串"let hero of heroes"的含义是:
取出heroes数组中的每个英雄,把它存入局部变量hero中,并在每次迭代时对模板 HTML 可用

模板输入变量

hero前的let关键字创建了一个名叫hero的模板输入变量。 ngFor指令在由父组件的heroes属性返回的heroes数组上迭代,每次迭代都从数组中把当前元素赋值给hero变量。
<app-hero-detail *ngFor="let hero of heroes" [hero]="hero"></app-hero-detail>
这个例子表明对于heros数组需要迭代,然后它的每一个输入变量都需要传入给子组件hero-detail,所以添加了一个[hero]="hero",其实这个方式和下面的方式相同
<app-hero-detail *ngFor="let hero of heroes" [hero]="hero"></app-hero-detail>
只是由于hero需要从当前的数组中迭代,在NgFor表达式的外部获取不到,忽而出现上面的情况。

带索引的*ngFor

NgFor指令上下文中的index属性返回一个从零开始的索引,表示当前条目在迭代中的顺序。 我们可以通过模板输入变量捕获这个index值,并把它用在模板中。
<div *ngFor="let hero of heroes; let i=index">{{i + 1}} - {{hero.name}}</div>

带trackBy的*ngFor

还是先举个例子:
//angular.component.html
<ul (DOMNodeInserted)="domChange()">
  <li *ngFor="let item of collection">{{item.name}}</li>
</ul>
<button (click)="refrashNames()">Refresh names</button>
<button (click)="refrashIds()">Refresh ids</button>
//angular.component.ts
import {AfterViewChecked, Component, DoCheck, ElementRef, OnInit, ViewChild} from '@angular/core';

let count = 0;

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

export class AngularComponent {
  private collection: { id: number, name: string }[];

  @ViewChild('ul')
  ul1: ElementRef;

  constructor() {
    this.collection = [
      {
        id: 1,
        name: 'rodchen1'
      }];
  }

  trackByFn(index, item) {
    return item.id; // or item.id
  }

  refrashNames() {
    this.collection = [
      {
        id: 1,
        name: 'rodchen2'
      }];
  }

  refrashIds() {
    this.collection = [
      {
        id: 3,
        name: 'rodchen2'
      }];
  }

  domChange() {
    console.log('DOM更新');
  }
}
此时的代码,当我们点击两个button中的任意一个,ul和li标签都会重新渲染(自己测试的话会发现标签会刷一下)。如下图:红色框内的标签会重新渲染。蓝色框内是我们绑定的一个DOM插入的方法,用于追踪DOM的变化。


如果我们使用了trackBy,这个会用来返回NgFor应该追踪的值,这里的例子中是说只有当id变化才会重新渲染DOM.
//angular.component.html
<ul (DOMNodeInserted)="domChange()">
  <li *ngFor="let item of collection; trackBy: trackByFn">{{item.name}}</li>
</ul>
<button (click)="refrashNames()">Refresh names</button>
<button (click)="refrashIds()">Refresh ids</button>
此时会发生什么呢?第一个button是改变name,第二个button是改变id,当我们点击第一个button的时候不会发生DOM重新渲染,当我们点击第二个button,只有第一次的时候才会渲染,因为第一个改变了id的值。

c) NgSwitch

NgSwitch指令类似于JavaScript的switch语句。 它可以从多个可能的元素中根据switch条件来显示某一个。 Angular只会把选中的元素放进DOM中。
NgSwitch实际上包括三个相互协作的指令:NgSwitch、NgSwitchCase 和 NgSwitchDefault,例子如下:
<div [ngSwitch]="currentHero.emotion">
  <app-happy-hero    *ngSwitchCase="'happy'"    [hero]="currentHero"></app-happy-hero>
  <app-sad-hero      *ngSwitchCase="'sad'"      [hero]="currentHero"></app-sad-hero>
  <app-confused-hero *ngSwitchCase="'confused'" [hero]="currentHero"></app-confused-hero>
  <app-unknown-hero  *ngSwitchDefault           [hero]="currentHero"></app-unknown-hero>
</div>

(3) 自定义属性型指令


a) 自定义属性型指令

自定义属性型指令至少需要一个带有@Directive装饰器的控制器类。该装饰器指定了一个用于标识属性的选择器。 控制器类实现了指令需要的指令行为。
import { Directive, ElementRef, Input } from '@angular/core';


@Directive({ selector: '[appHighlight]' })
export class HighlightDirective {
    constructor(el: ElementRef) {
       el.nativeElement.style.backgroundColor = 'yellow';
    }
}
import语句指定了从 Angular 的core库导入的一些符号。

Directive提供@Directive装饰器功能。
ElementRef注入到指令构造函数中。这样代码就可以访问 DOM 元素了。
Input将数据从绑定表达式传达到指令中。


然后,@Directive装饰器函数以配置对象参数的形式,包含了指令的元数据。
@Directive装饰器需要一个 CSS 选择器,以便从模板中识别出关联到这个指令的 HTML。
用于 attribute 的 CSS 选择器就是属性名称加方括号。 这里,指令的选择器是[myHighlight],Angular 将会在模板中找到所有带myHighlight属性的元素。

b) 响应用户引发的事件 

当前,myHighlight只是简单的设置元素的颜色。 这个指令应该在用户鼠标悬浮一个元素时,设置它的颜色。
具体内容请查看文章

c) 使用数据绑定向指令传递值

向指令传递数据使用@Input,请查看组件

(4) 自定义结构指令

先介绍两个名词:
TemplateRef:用于表示内嵌的 template 模板元素,通过 TemplateRef 实例,我们可以方便创建内嵌视图(Embedded Views),且可以轻松地访问到通过 ElementRef 封装后的 nativeElement。需要注意的是组件视图中的 template 模板元素,
ViewContainerRef:用于表示一个视图容器,可添加一个或多个视图。通过 ViewContainer
Ref 实例,我们可以基于 TemplateRef 实例创建内嵌视图,并能指定内嵌视图的插入位置,也可以方便对视图容器中已有的视图进行管理。简而言之,ViewContainerRef 的主要作用是创建和管理内嵌视图或组件视图。

然后举个例子实现*NgIf指令

import {Directive, Input, TemplateRef, ViewContainerRef} from '@angular/core';

@Directive({
  selector: '[appIf]'
})
export class IfDirective {
  @Input('appIf')
  set condition(newCondition: boolean) { // set condition
    if (newCondition) {
      this.viewContainer.createEmbeddedView(this.templateRef);
    } else {
      this.viewContainer.clear();
    }
  }
  constructor(private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef) {
  }
}


<div *appIf="false">
  test direcive
</div>


2. 指令生命周期概述

指令与组件共有的钩子

ngOnChanges
ngOnInit
ngDoCheck
ngOnDestroy

组件特有的钩子
ngAfterContentInit
ngAfterContentChecked
ngAfterViewInit
ngAfterViewChecked


生命周期的顺序 Lifecycle sequence

当Angular使用构造函数新建一个组件或指令后,就会按下面的顺序在特定时刻调用这些生命周期钩子方法:

子组件

//life.component.ts
import {
  AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, Component, DoCheck, Input, OnChanges, OnDestroy,
  OnInit, SimpleChanges
} from '@angular/core';

let logIndex: number = 1;

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

export class LifeComponent implements OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {

  @Input()
  name: string;

  logIt(msg: string) {
    console.log(`#${logIndex++} ${msg}`);
  }

  constructor() {
    this.logIt('constructor: name属性的值是:' + name);
  }

  ngOnInit() {
    this.logIt('ngOnInit');
  }

  ngOnChanges(changes: SimpleChanges): void {
    let name = changes['name'].currentValue;
    this.logIt('ngOnChanges: name属性的值是:' + name);
  }

  ngDoCheck(): void {
    this.logIt('ngDoCheck');
  }

  ngAfterContentInit(): void {
    this.logIt('ngAfterContentInit');
  }

  ngAfterContentChecked(): void {
    this.logIt('ngAfterContentChecked');
  }

  ngAfterViewInit(): void {
    this.logIt('ngAfterViewInit');
  }

  ngAfterViewChecked(): void {
    this.logIt('ngAfterViewChecked');
  }

  ngOnDestroy(): void {
    this.logIt('ngOnDestroy');
  }
}
父组件
//app.component.html
<app-life [name]="title"></app-life>
<input type="text" [(ngModel)]="title" />
父组件控制title,来动态改变子组件的输入值,用来将ngOnChanges打在控制台上。

如上图所示:初始执行的结果为空色框,执行顺序是构造函数先执行。然后在按照上面图中顺序执行。然后当我修改input内的值的时候,会执行黄色和绿色框内的方法。也可以看出生命周期内,有些方法是只执行一次的,有的是可以多次执行的。之前也已经介绍过了。这个例子中没有打出ngOnDestroy,后面会介绍的。

接下来就一一介绍生命周期内的各个函数。


3. 解析指令生命周期

(1) constructor

组件的构造函数会在所有的生命周期钩子之前被调用,它主要用于依赖注入或执行简单的数据初始化操作。构造函数本质上不应该算作Angular的钩子。从上面的自理可以看到对于输入属性在构造函数执行期间是没有赋值的。

(2)ngOnChanges

当数据绑定输入属性的值发生变化的时候,Angular 将会主动调用 ngOnChanges 方法。它会获得一个 SimpleChanges 对象,包含绑定属性的新值和旧值,它主要用于监测组件输入属性的变化。
将上面的方法修改一下,就可以打出新值和旧值
ngOnChanges(changes: SimpleChanges): void {
	let originalName = changes['name'].previousValue;
	let currentName = changes['name'].currentValue;
	this.logIt('ngOnChanges: name属性的原先值是:' + originalName + ', name属性现在的值是:' + currentName );
}

重点: 对于输入属性的检测,如果输入属性是一个基本数据类型的话,变化是会检测到。如果输入属性是一个对象,我们只改变其中的属性,此方法是捕捉不到的。请看实例:
//child.component.ts
import {Component, DoCheck, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core';

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit, OnChanges, DoCheck {
  @Input()
  greeting: string;

  @Input()
  user: {name: string};

  constructor() { }

  ngOnInit() {
  }

  ngOnChanges(changes: SimpleChanges): void {
    console.log(JSON.stringify(changes, null, 2));
  }

  ngDoCheck(): void {
  }
}
//child.component.html
<div class="child">
  <h2>我是子组件</h2>
  <div>
    问候语: {{greeting}}
  </div>
  <div>
    姓名:{{user.name}}
  </div>
</div>
//app.component.html
<div class="parent">
  <h2>我是父组件</h2>
  <div>
    问候语: <input type="text" [(ngModel)]="greeting"/>
  </div>
  <div>
    姓名:<input type="text" [(ngModel)]="user.name"/>
  </div>
  <app-child [greeting]="greeting" [user]="user"></app-child>
</div>
页面上所示如下图:


红色框内是组件初始的执行,由于previousValue都是为undefined,所以控制台没有打出来。接下来开始我们的测试。

首先我们改变greeting的输入:


如图所示,此时onchange方法捕获到了输入属性的变化,并且打出来之前的值和现在的值。
接下来我们修改一下user.name的值:

从上面的图中可以看出此时onchange是没有执行的,但是子组件的内容重新渲染了,这是为什么呢?这是由于angular的变更检测机制导致的。
现在我们修改一下上面的ngDoCheck,将每一次变化之后的user.name打出来:


如图所示,可以看出每次的变化变更检测机制会检测到。

(3) ngOnInit

在第一次 ngOnChanges 执行之后调用,并且只被调用一次。它主要用于执行组件的其它初始化操作或获取组件输入的属性值。

使用ngOnInit()有两个原因:
在构造函数之后马上执行复杂的初始化逻辑
在Angular设置完输入属性之后,对该组件进行准备。


(4) ngDoCheck

当组件的输入属性发生变化时,将会触发 ngDoCheck 方法。我们可以使用该方法,自定义我们的检测逻辑。它也可以用来加速我们变化检测的速度。
使用DoCheck钩子来检测那些Angular自身无法捕获的变更并采取行动。用这个方法来检测那些被Angular忽略的更改。
虽然ngDoCheck()钩子可以可以监测到英雄的name什么时候发生了变化。但我们必须小心。 这个ngDoCheck钩子被非常频繁的调用 —— 在每次变更检测周期之后,发生了变化的每个地方都会调它。

这里现在不去深挖,因为本人还没有去了解到变更检测的内容,所以这个方法留到后面去讲解。

(4) ngAfterViewInit & ngAfterViewChecked

对于这两个钩子: 主要是和@viewchild相关,具体内容请看组件

子组件:
//child-view.component.html
<p>
  child-view works!
</p>
//child-view.component.ts
import {AfterViewChecked, AfterViewInit, Component, OnInit} from '@angular/core';

@Component({
  selector: 'app-child-view',
  templateUrl: './child-view.component.html',
  styleUrls: ['./child-view.component.css']
})
export class ChildViewComponent implements OnInit, AfterViewInit, AfterViewChecked {
  constructor() { }

  ngOnInit() {
  }

  ngAfterViewInit(): void {
    console.log("子组件的试图初始化完毕!");
  }

  ngAfterViewChecked(): void {
    console.log("子组件的试图变更检测完毕!");
  }

  greeting(name: string) {
    console.log("hellow" + name);
  }
}
父组件
//app.component.html
<app-child-view #child1></app-child-view>
<app-child-view #child2></app-child-view>
<button (click)="child2.greeting('child2')">调用child2的方法</button>
//app.component.ts
import {AfterViewChecked, AfterViewInit, Component, OnInit, ViewChild} from '@angular/core';
import {PriceQuote} from './price-quote/price-quote.component';
import {ChildViewComponent} from './child-view/child-view.component';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, AfterViewInit, AfterViewChecked {
  title = 'app';

  ngOnInit(): void {
  }

  ngAfterViewInit(): void {
    console.log("父组件的试图初始化完毕!");
  }

  ngAfterViewChecked(): void {
    console.log("父组件的试图变更检测完毕!");
  }
}
页面初始化之后,然后我们点击几次button。

如图所示:在所有的子组件视图变更检测完毕之后才会初始化父组件的视图。然后每一次当我们点击button的时候,都会先触发子组件的变更检测,在触发父组件的变更检测。
这里说一个重点: 如果我们在这两个方法中对渲染在模板的值进行赋值会发生错误。修改父组件如下,添加一个message的信息。
<app-child-view #child1></app-child-view>
<app-child-view #child2></app-child-view>
<button (click)="child2.greeting('child2')">调用child2的方法</button>
{{message}}
import {AfterViewChecked, AfterViewInit, Component, OnInit, ViewChild} from '@angular/core';
import {PriceQuote} from './price-quote/price-quote.component';
import {ChildViewComponent} from './child-view/child-view.component';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, AfterViewInit, AfterViewChecked {
  title = 'app';

  message: string = 'test';

  ngOnInit(): void {
  }

  ngAfterViewInit(): void {
    console.log("父组件的试图初始化完毕!");
    this.message = 'rodchen';
  }

  ngAfterViewChecked(): void {
    console.log("父组件的试图变更检测完毕!");
  }
}
然后看一下页面:


可以看到这个地方出错了:Angular的“单向数据流”规则禁止在一个视图已经被组合好之后再更新视图。 而这两个钩子都是在组件的视图已经被组合好之后触发的。不允许再去修改视图,可以使用setTimeout去执行。因为setTimeout的执行是基于事件循环的,它的执行已经是当前事件执行完了之后再去执行,可以查看 文章

(5) ngAfterContentInit & ngAfterContentChecked

对于这两个钩子: 主要是和@ContentChild相关,具体内容请看组件
例子就不举了,直接看一下页面输出好了:

可以看到是父组件的页面内容先执行,然后在执行子组件的页面内容,这也是因为子组件内容的投影导致。这里只说一个重点,这两个方法的执行在(4)的前面,而且在这两个方法执行的过程中可以去修改页面的内容。

(6) ngOnDestroy 

当Angular每次销毁指令/组件之前调用并清扫。 在这儿反订阅可观察对象和分离事件处理器,以防内存泄漏。在Angular销毁指令/组件之前调用。
这个钩子的主要作用是为了销毁一定的资源。也比较简单就不多说了,它是由路由的变化导致的,当从一个路由调到另一个路由的时候,前一个路由的组件就会被销毁。

猜你喜欢

转载自blog.csdn.net/it_rod/article/details/79433887