Angular 结构指令

结构指令是处理组件或元素通过使用模板标签呈现的方式。 这允许我们运行一些代码来决定最终渲染的输出。 Angular 2有一些内置的结构指令,如ngIfngForngSwitch

注意:对于不熟悉模板标记的用户,它是一个具有几个特殊属性的HTML元素。 嵌入在模板标记中的内容不会在网页加载时呈现,而是在运行时通过代码加载的内容。 有关模板标记的更多信息,请访问MDN文档

结构化指令在模板中有自己的特殊语法,它们作为语法糖。

@Component({
  selector: 'app-directive-example',
  template: `
    <p *structuralDirective="expression">
      Under a structural directive.
    </p>
  `
})

我们的伪结构伪指令不带方括号,而是带有星号。 注意,即使没有方括号,绑定仍然是表达式绑定。 这是因为它是语法糖,允许以更直观的方式使用指令,类似于在Angular 1中使用指令。上面的组件模板等价于以下:

@Component({
  selector: 'app-directive-example',
  template: `
    <template [structuralDirective]="expression">
      <p>
        Under a structural directive.
      </p>
    </template>
  `
})

这里,我们看到我们前面说的结构化指令使用的template 标签。 Angular 2也有一个做同样的事情的内置template 指令。

@Component({
  selector: 'app-directive-example',
  template: `
    <p template="structuralDirective expression">
      Under a structural directive.
    </p>
  `
})

NgIf 指令

ngIf指令根据表达式是真还是假有条件地呈现组件或元素。这里是我们的应用程序组件,我们将ngIf指令绑定到一个示例组件。

@Component({
  selector: 'app',
  template: `
    <button type="button" (click)="toggleExists()">Toggle Component</button>
    <hr/>
    <if-example *ngIf="exists">
      Hello
    </if-example>
  `
})
export class AppComponent {
  exists: boolean = true;
  toggleExists() {
    this.exists = !this.exists;
  }
}

查看示例

单击按钮将切换IfExampleComponent是否为DOM的一部分,而不仅仅是它是否可见。 这意味着每次点击按钮时,IfExampleComponent将被创建或销毁。 这可能是有昂贵的创建/销毁操作的组件的问题。 例如,一个组件可以有一个大的子子树或构造时进行多个HTTP调用。 在这些情况下,如果可能,最好避免使用ngIf。

⚡️ngIf 语法在 4.0 后已经支持else。此外,条件值现在可以存储在局部变量中,供以后重用。当与async管道一起使用时特别有用。

<div *ngIf="userObservable | async; else loading; let user">
  Hello {{user.last}}, {{user.first}}!
</div>
<template #loading>Waiting...</template>

注意当使用 then 时 *ngIf 所在的元素部分将被忽略。

<element *ngIf="someExpression then thenRef else elseRef"></element>
<template #thenRef>AAA</template>
<template #elseRef>BBB</template>

NgFor 指令

ngFor指令是通过使用可迭代的每个项作为模板的上下文来重复模板的一种方式。

@Component({
  selector: 'app-root',
  template: `
    <app-for-example *ngFor="let episode of episodes" [episode]="episode">
      {{episode.title}}
    </app-for-example>
  `
})
export class AppComponent {
  episodes = [
    { title: 'Winter Is Coming', director: 'Tim Van Patten' },
    { title: 'The Kingsroad', director: 'Tim Van Patten' },
    { title: 'Lord Snow', director: 'Brian Kirk' },
    { title: 'Cripples, Bastards, and Broken Things', director: 'Brian Kirk' },
    { title: 'The Wolf and the Lion', director: 'Brian Kirk' },
    { title: 'A Golden Crown', director: 'Daniel Minahan' },
    { title: 'You Win or You Die', director: 'Daniel Minahan' },
    { title: 'The Pointy End', director: 'Daniel Minahan' }
  ];
}

View Example

ngFor指令与我们看到的其他指令有不同的语法。 如果你熟悉for...of语句,你会注意到,他们几乎相同。 ngFor允许您指定要迭代的iterable对象,以及在范围内引用每个项的名称。 在我们的示例中,您可以看到该episode 可用于插值以及属性绑定。 该指令做一些额外的解析,所以当它扩展到模板形式,它看起来有点不同:

@Component({
  selector: 'app',
  template: `
    <template ngFor [ngForOf]="episodes" let-episode>
      <app-for-example [episode]="episode">
        {{episode.title}}
      </app-for-example>
    </template>
  `
})

View Example

请注意,模板元素上有一个奇怪的let-episode属性。 ngFor指令在其范围内提供一些变量作为上下文。 let-episode是一个上下文绑定,这里它接受迭代的每个项的值。

局部变量

NgFor还提供了可以被局部变量别名的其他值:

  • index - 当前项目的位置在从0开始的迭代中
  • first - 如果当前项是可迭代中的第一个项,则为true
  • last - 如果当前项是可迭代中的最后一个项,则为true
  • even - 如果当前索引为偶数,则为true
  • odd - 如果当前索引是奇数,则为true
@Component({
  selector: 'app',
  template: `
    <for-example
      *ngFor="let episode of episodes; let i = index; let isOdd = odd"
      [episode]="episode"
      [ngClass]="{ odd: isOdd }">
      {{i+1}}. {{episode.title}}
    </for-example>
    <hr/>
    <h2>Desugared</h2>
    <template ngFor [ngForOf]="episodes" let-episode let-i="index" let-isOdd="odd">
      <for-example [episode]="episode" [ngClass]="{ odd: isOdd }">
        {{i+1}}. {{episode.title}}
      </for-example>
    </template>
  `
})

View Example

trackBy

通常ngFor用于迭代具有唯一ID字段的对象列表。 在这种情况下,我们可以提供一个trackBy函数,帮助Angular跟踪列表中的项目,以便它可以检测哪些项目已添加或删除,并提高性能。

Angular 2将通过引用来尝试和跟踪对象,以确定应该创建和销毁哪些项目。 然而,如果你用一个新的对象源代替列表,也许是一个API请求的结果,我们可以通过告诉Angular 2如何跟踪事情来获得一些额外的性能。

例如,如果Add Episode按钮是要发出请求并返回新的剧集列表,我们可能不想销毁并重新创建列表中的每个项目。 如果剧集有唯一的ID,我们可以添加一个trackBy函数:

@Component({
  selector: 'app-root',
  template: `
  <button
    (click)="addOtherEpisode()"
    [disabled]="otherEpisodes.length === 0">
    Add Episode
  </button>
  <app-for-example
    *ngFor="let episode of episodes;
    let i = index; let isOdd = odd;
    trackBy: trackById" [episode]="episode"
    [ngClass]="{ odd: isOdd }">
    {{episode.title}}
  </app-for-example>
  `
})
export class AppComponent {
  otherEpisodes = [
    { title: 'Two Swords', director: 'D. B. Weiss', id: 8 },
    { title: 'The Lion and the Rose', director: 'Alex Graves', id: 9 },
    { title: 'Breaker of Chains', director: 'Michelle MacLaren', id: 10 },
    { title: 'Oathkeeper', director: 'Michelle MacLaren', id: 11 }]
  episodes = [
    { title: 'Winter Is Coming', director: 'Tim Van Patten', id: 0 },
    { title: 'The Kingsroad', director: 'Tim Van Patten', id: 1 },
    { title: 'Lord Snow', director: 'Brian Kirk', id: 2 },
    { title: 'Cripples, Bastards, and Broken Things', director: 'Brian Kirk', id: 3 },
    { title: 'The Wolf and the Lion', director: 'Brian Kirk', id: 4 },
    { title: 'A Golden Crown', director: 'Daniel Minahan', id: 5 },
    { title: 'You Win or You Die', director: 'Daniel Minahan', id: 6 }
    { title: 'The Pointy End', director: 'Daniel Minahan', id: 7 }
  ];
  addOtherEpisode() {
    // We want to create a new object reference for sake of example
    let episodesCopy = JSON.parse(JSON.stringify(this.episodes))
    this.episodes=[...episodesCopy,this.otherEpisodes.pop()];
  }
  trackById(index: number, episode: any): number {
    return episode.id;
  }
}

要了解这会如何影响ForExample组件,我们向它添加一些 console。

export class ForExampleComponent {
  @Input() episode;
  ngOnInit() {
    console.log('component created', this.episode)
  }
  ngOnDestroy() {
    console.log('destroying component', this.episode)
  }
}

View Example

当我们查看示例时,当我们点击Add Episode时,我们可以看到控制台输出,指示只有一个组件被创建 - 用于新添加到列表中的项目。

但是,如果我们从*ngFor中删除trackBy - 每次我们单击按钮,我们将看到组件中的项目被销毁和重新创建。

View Example Without trackBy

NgSwitch 指令

ngSwitch实际上包括两个指令,一个属性指令和一个结构指令。 它非常类似于JavaScript和其他编程语言中的switch语句,但是在模板中。

@Component({
  selector: 'app-root',
  template: `
    <div class="tabs-selection">
      <app-tab [active]="isSelected(1)" (click)="setTab(1)">Tab 1</app-tab>
      <app-tab [active]="isSelected(2)" (click)="setTab(2)">Tab 2</app-tab>
      <app-tab [active]="isSelected(3)" (click)="setTab(3)">Tab 3</app-tab>
    </div>
    <div [ngSwitch]="tab">
      <app-tab-content *ngSwitchCase="1">Tab content 1</app-tab-content>
      <app-tab-content *ngSwitchCase="2">Tab content 2</app-tab-content>
      <app-tab-content *ngSwitchCase="3"><app-tab-3></app-tab-3></app-tab-content>
      <app-tab-content *ngSwitchDefault>Select a tab</app-tab-content>
    </div>
  `
})
export class AppComponent {
  tab: number = 0;
  setTab(num: number) {
    this.tab = num;
  }
  isSelected(num: number) {
    return this.tab === num;
  }
}

View Example

这里我们看到ngSwitch属性指令附加到一个元素。 该表达式绑定到指令定义什么将在 switch 指令中进行比较。 如果绑定到ngSwitchCase的表达式匹配给予ngSwitch的表达式,那么将创建这些组件,并销毁其他组件。 如果没有匹配的情况,则会创建与其绑定的ngSwitchDefault的组件,其他组件将被销毁。 注意,可以使用ngSwitchCase来匹配多个组件,在这些情况下,将创建所有匹配的组件。 由于组件被创建或销毁,请注意这样做的成本。

使用多个结构指令

有时我们想要将多个结构指令结合在一起,例如使用ngFor进行迭代,但想要执行ngIf以确保值匹配一些或多个条件。 组合结构指令可能会导致意想不到的结果,因此Angular要求模板一次只能绑定到一个指令。 要应用多个指令,我们必须扩展含糖语法或嵌套模板标签。

@Component({
  selector: 'app-root',
  template: `
    <template ngFor [ngForOf]="[1,2,3,4,5,6]" let-item>
      <div *ngIf="item > 3">
        {{item}}
      </div>
    </template>
  `
})

View Example

如果选项卡标题和内容被抽象到组件类中,下面的选项卡示例可以使用ngForngSwitch

import {Component} from '@angular/core';
@Component({
  selector: 'app-root',
  template: `
    <div class="tabs-selection">
      <tab
        *ngFor="let tab of tabs; let i = index"
        [active]="isSelected(i)"
        (click)="setTab(i)">
        {{ tab.title }}
      </tab>
    </div>
    <div [ngSwitch]="tabNumber">
      <template ngFor [ngForOf]="tabs" let-tab let-i="index">
        <tab-content *ngSwitchCase="i">
          {{tab.content}}
        </tab-content>
      </template>
      <tab-content *ngSwitchDefault>Select a tab</tab-content>
    </div>
  `
})
export class AppComponent {
  tabNumber: number = -1;
  tabs = [
    { title: 'Tab 1', content: 'Tab content 1' },
    { title: 'Tab 2', content: 'Tab content 2' },
    { title: 'Tab 3', content: 'Tab content 3' },
  ];
  setTab(num: number) {
    this.tabNumber = num;
  }
  isSelected(num: number) {
    return this.tabNumber === i;
  }
}

View Example