Drzewo komponentów

Na aplikację Angular można patrzeć jak na drzewo zagnieżdżonych komponentów. Na przykład:

<tag-todo-app>
  <tag-todo-list>
    <tag-todo-item></tag-todo-item>
    <tag-todo-item></tag-todo-item>
    <tag-todo-item></tag-todo-item>
  </tag-todo-list>
  <tag-todo-form></tag-todo-form>
</tag-todo-app>

W katalogu głównym mamy tag-todo-app, która składa się ztag-todo-list oraz tag-todo-form. Na liście mamy kilka tag-todo-item. Każdy z tych komponentów jest widoczny dla użytkownika, który może wchodzić w interakcje z tymi komponentami i wykonywać działania.

Przekazywanie danych do komponentu

Istnieją dwa sposoby przekazywania danych do komponentu za pomocą powiązania właściwości i powiązania zdarzeń. W Angular wykrywanie zmian danych i zdarzeń odbywa się z góry na dół od rodzica do dzieci. Jednak w przypadku zdarzeń Angular może skorzystać z modelu zdarzeń DOM, w którym wydarzenia przepływają od dołu do góry, od dziecka do rodzica.

Dekorator @Input() definiuje zestaw parametrów, które można przekazać z nadrzędnego komponentu. Na przykład możemy zmodyfikować komponent HelloComponent, abyname mogło być dostarczone przez obiekt nadrzędny.

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

@Component({
  selector: 'tag-hello',
  template: '<p>Witaj, {{name}}!</p>',
})
export class HelloComponent {
  @Input() name: string;
}

Powodem tworzenia komponentów jest nie tylko hermetyzacja, ale także możliwość ponownego wykorzystania. Wejścia pozwalają nam skonfigurować konkretne wystąpienie komponentu.

Możemy teraz korzystać z naszego komponentu w następujący sposób:

<div>
<!-- Powiązanie z łańcuchem znaów -->
<tag-hello name="Świecie"></tag-hello>
<!-- Powiązanie ze zmienną rodzica -->
<tag-hello [name]="helloName"></tag-hello>
</div>

Zwróć uwagę na możliwość wiązania wartości wejścia (@Input) potomka ze zmienną (w przykładzie: helloName).

Zobacz przykład [004]

W przeciwieństwie do Angular 1.x jest to jednokierunkowe wiązanie.

Reagowanie na zdarzenia w komponencie

Procedura obsługi zdarzenia jest określona w szablonie za pomocą nawiasów okrągłych: (zdarzenie)="funkcja()". Funkcja obsługi zdarzeń jest następnie kodowana w klasie komponentu.

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

@Component({
  selector: 'tag-counter',
  template: `
    <div>
      <p>Count: {{num}}</p>
      <button (click)="increment()">Zwiększ</button>
    </div>
  `
})
export class CounterComponent {
  num = 0;

  increment() {
    this.num++;
  }
}

Zobacz przykład [005]

Aby wysłać dane z komponentów przez wyjścia, stosujemy dekorator Output.

Uwaga: alternatywą dla takiego sposobu pisania aplikacji jest użycie wspólnego dla wszystkich komponentów stanu (vide projekt Redux). Bowiem przekazywanie zdarzeń z dołu ku górze znacząco komplikuje program.

Wyjście przyjmuje listę parametrów, które komponent udostępnia swojemu rodzicowi.

app/counter.component.ts

import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'tag-counter',
  templateUrl: 'counter.component.html'
})
export class CounterComponent {
  @Input()  count = 0;
  @Output() result = new EventEmitter<number>();

  increment() {
    this.count++;
    this.result.emit(this.count);
  }
}

Do przekazywania nietypowych danych służy klasa EventEmitter który jest rozszerzeniem klasy Subject (z biblioteki RxJS). Może ona rozsyłać zdarzenia niestandardowe przy użyciu swojej metody emit() - jak w powyższym przykładzie. Rozsyłanie następuje w funkcji obsługi zdarzenia (increment) komponentu. Domyślnie nazwa zdarzenia niestandardowego jest taka sama jak nazwa wyjścia (tu: result). Dekorator @Output może ją zmienić.

app/counter.component.html

<div>
  <p>Licznik: {{ count }}</p>
  <button (click)="increment()">Zwiększ</button>
</div>

app/app.component.ts

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

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html'
})
export class AppComponent implements OnChanges {
  num = 1;
  parentCount = 0;

  ngOnChange(val: number) {
    this.parentCount = val;
  }
}

Zdarzenie ngOnChanges jest wywoływane, gdy następuje zmiana jakiejś własności wejściowej (@Input - zob. cykl życia komponentu). Aby je obsłużyć, musimy dodać do deklaracji klasy interfejs o nazwie która odpowiada nazwie funkcji obsługi zdarzenia bez ng. W tym przypadku ngOnChanges() => OnChanges.

app/app.component.html

<div>
  Rodzic - liczba: {{ num }}<br>
  Rodzic - licznik: {{ parentCount }}
  <tag-counter [count]="num" (result)="ngOnChange($event)">
  </tag-counter>
</div>

Zmienna $event reprezentuje zdarzenie. Może to być natywne zdarzenie przeglądarki (zob. https://developer.mozilla.org/en-US/docs/Web/Events\ lub własne zdarzenie - tu "result".

W szablonach używamy nawiasów klamrowych [] do przekazywania danych wejściowych i zwykłych nawiasów () do obsługi wyjść.

Zobacz przykład [006]

Razem zestaw powiązań wejścia-wyjścia definiuje publiczny interfejs API twojego komponentu.