Dyrektywy strukturalne

Dyrektywy strukturalne pozwalają na modyfikację procesu renderowania komponentu. Angular ma kilka wbudowanych dyrektyw strukturalnych, odpowiadających instrukcjom JavaScript: ngIf,ngFor i ngSwitch.

Dyrektywy strukturalne mają własną specjalną składnię w szablonie (lukier syntaktyczny).

@Component({
  selector: 'app-directive-example',
  template: `
    <p *structuralDirective="expression">
      blok zawarty w obrębie dyrektywy.
    </p>
  `
})

Zamiast być ujęte w nawiasy kwadratowe, nasza fikcyjna dyrektywa strukturalna jest poprzedzana gwiazdką. Zauważ, że powiązanie nadal jest wiązaniem wyrażenia, nawet jeśli nie ma nawiasów kwadratowych. Wspomniany "lukier syntaktyczny" zastępuje rzeczywisty zapis (z nawiasami). Zapis dyrektywy z gwiazdką jest bardziej intuicyjny i podobny do sposobu użycia dyrektyw w Angular 1. Powyższy szablon komponentu jest równoważny z następującym:

@Component({
  selector: 'app-directive-example',
  template: `
    <ng-template [structuralDirective]="expression">
      <p>
      blok zawarty w obrębie dyrektywy.
      </p>
    </ng-template>
  `
})

Uwaga! Początkowo (Angular 2) nie stosowano znacznika ng-template, tylko template (można to znaleźć w starszych podręcznikach).

Widzimy przy okazji, że dyrektywy strukturalne używają znacznika ng-template. Angular ma również wbudowaną dyrektywę template, która robi to samo:

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

Zobacz przykład: [012]

Dyrektywa NgIf

Dyrektywa ngIf warunkowo dodaje lub usuwa węzeł DOM na podstawie tego, czy wyrażenie jest prawdziwe, czy nie.

Oto nasz komponent aplikacji, w którym używamy dyrektywę ngIf do włączenia komponentu przykładowego.

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

  toggleExists() {
    this.exists = !this.exists;
  }
}

Zobacz przykład [012]

Kliknięcie przycisku spowoduje przełączenie, czy IfExampleComponent (implementujący if-example) istnieje w DOM, a nie tylko to, czy jest widoczne, czy nie!. Oznacza to, że za każdym razem, gdy przycisk zostanie kliknięty, IfExampleComponent zostanie utworzony lub zniszczony. Może to stwarzać problem w przypadku komponentów, które są złożone ("drogie" tworzenie/niszczenie). W takich przypadkach lepiej jest unikać użycia ngIf.

Dyrektywa NgFor

Dyrektywa NgFor jest sposobem na powtórzenie szablonu z możliwością użycia w nim iterowanego w pętli elementu (tak jak w pętli for/of):

@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' }
  ];
}

Zobacz przykład [012]

Dyrektywa NgFor ma inną składnię niż inne dyrektywy, które widzieliśmy. Jeśli znasz już instrukcję for ... of, zauważ, że są prawie identyczne. NgFor pozwala określić obiekt iteracyjny (lista) i nazwę, pod którą będzie występował każdy element tego obiektu / listy. W naszym przykładzie jest to episode. Dyrektywa robi dodatkowe przetwarzanie, więc gdy jest ona rozszerzona do postaci szablonu, wygląda nieco inaczej:

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

Zobacz przykład [012]

Zwróć uwagę na własność let-episode elemntu ng-template. Dyrektywa NgFor wykorzystuje niektóre zmienne w swoim kontekście / zasięgu. W taki sposób można zapisywać utworzenie zmiennej episode (let-episode ). Jak widać skłądnia z gwiazdką jest dużo bardziej intuicyjna,

Zmienne lokalne

NgFor dostarcza również inne wartości, które mogą być użyte jako zmienne lokalne:

  • index - pozycja aktualnej pozycji w iteracji zaczynająca się od 0
  • first - true, jeśli bieżący element jest pierwszym elementem w iteracji
  • last - true, jeśli bieżący element jest ostatnim elementem w iteracji
  • even - true, jeśli bieżący indeks jest liczbą parzystą
  • odd - true, jeśli bieżący indeks jest liczbą nieparzystą
@Component({
  selector: 'app-root',
  template: `
    <app-for-example
      *ngFor="let episode of episodes; let i = index; let isOdd = odd"
      [episode]="episode"
      [ngClass]="{ odd: isOdd }">
      {{i+1}}. {{episode.title}}
    </app-for-example>

    <hr>

    <h2>Desugared</h2>

    <ng-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>
    </ng-template>
  `
})

Ten przykład pokazuje przy okazji naturalne użycie dyrektywy ngClass powiązanej z obiektem (warunkowe określenie stylu).

Zobacz przykład [058]

trackBy

Często NgFor służy do iteracji poprzez listę obiektów z unikalnym polem ID. W takim przypadku możemy udostępnić funkcję trackBy, która pomaga Angularowi śledzić elementy na liście, dzięki czemu może wykryć, które elementy zostały dodane lub usunięte i poprawić wydajność.

Angular radzi sobie bez tego, ale gdy stworzys własną listę obiektów, przekazywaną przez API, być możemy stworzyć bardziej optymalne mechanizmy.

Na przykład, jeśli przycisk Add Episode może uzyskać nową listę epizodów (odcinków serialu), ale możemy nie chcieć niszczyć i ponownie tworzyć wszystkich elementów na liście. Jeśli epizody mają unikalny identyfikator, możemy dodać funkcję 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;
  }
}

Aby zobaczyć, jak może to wpłynąć na komponent ForExample, dodajmy do niego trochę logowania.

export class ForExampleComponent {
  @Input() episode;

  ngOnInit() {
    console.log('component created', this.episode)
  }
  ngOnDestroy() {
    console.log('destroying component', this.episode)
  }
}

Zobacz przykład [059]

Kiedy oglądamy przykład, gdy klikamyAdd Episode widzimy wyjście konsoli wskazujące, że został utworzony tylko jeden komponent - dla nowo dodanego elementu do listy.

Jednakże, gdybyśmy usunęli trackBy z* ngFor - za każdym razem, gdy klikniemy przycisk, zobaczymy, że elementy w komponencie zostaną zniszczone i odtworzone.

Zobacz przykład bez trackBy [059a]

Dyrektywy NgSwitch

ngSwitch składa się z dwóch dyrektyw: dyrektywy atrybutów i dyrektywy strukturalnej. Jest bardzo podobny do instrukcji switch w JavaScript i innych językach programowania.

@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;
  }
}

Zobacz przykład [061]

Widzimy, jak dyrektywa atrybutu ngSwitch jest dołączona do elementu. Wyrażenie związane z dyrektywą określa, co będzie porównywane w dyrektywach strukturalnych i ngSwitchCase. Jeśli wyrażenie związane z ngSwitchCase pasuje do wyrażenia zngSwitch, komponenty te są tworzone, a pozostałe niszczone. Jeśli żaden z przypadków nie jest zgodny, zostaną utworzone komponenty przypisane do ngSwitchDefault. Zauważ, że wiele komponentów można dopasować za pomocą ngSwitchCase i w takich przypadkach zostaną utworzone wszystkie pasujące komponenty. Ponieważ komponenty są tworzone lub niszczone, należy pamiętać o kosztach.

Korzystanie z wielu dyrektyw strukturalnych

Czasami będziemy chcieli łączyć ze sobą wiele dyrektyw strukturalnych, takich jak iterowanie za pomocą ngFor, w połączeniu z ngIf. Łączenie dyrektyw strukturalnych może jednak prowadzić do nieoczekiwanych rezultatów, dlatego Angular dopuszcza szablony tylko z jedną dyrektywą strukturalną naraz. Aby zastosować wiele dyrektyw, będziemy musieli rozwinąć tagi oznaczające składnię (ng-template).

import {Component} from '@angular/core';

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

}

Zobacz przykład [062]

Tak haj poprzednim przykładzie na zakładkach można użyć ngFor ingSwitch.

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <div class="tabs-selection">
      <app-tab
        *ngFor="let tab of tabs; let i = index"
        [active]="isSelected(i)"
        (click)="setTab(i)">

        {{ tab.title }}
      </app-tab>
    </div>

    <div [ngSwitch]="tabNumber">
        <ng-template ngFor let-tab [ngForOf]="tabs" let-i="index">
        <app-tab-content *ngSwitchCase="i">
          {{tab.content}}
        </app-tab-content>
        </ng-template>
        <app-tab-content *ngSwitchDefault>Select a tab</app-tab-content>
    </div>
  `,
  styles: [`
    :host {
      font-family: Arial;
    }

    .tabs-selection {
      background-color: #ddd;
      display: flex;
      box-sizing: border-box;
      flex-direction: row;
      padding-left: 16px;
      padding-right: 16px;
      width: 100%;
    }
  `]
})
export class AppComponent {
  tabNumber = -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 === num;
  }
}

Zobacz przykład [063]