Koncepcyjnie
Powróćmy do projektu aplikacji z tamtego wpisu. Jest to lista kontaktów podzielona na komponenty:
Zacznijmy budowanie aplikacji od stworzenia potrzebnych komponentów przy pomocy Angular CLI. Główny komponent aplikacji już istnieje (domyślnie stworzony przez ng new {NAZWA PROJEKTU}
), wystarczy więc dodać contacts-list
, contact-item
i gravatar
. Do tego również można wykorzystać Angular CLI:
ng generate component contacts-list
ng g component contact-item
ng g component gravatar
Dodatkowo potrzebujemy serwis, w którym będą przechowywane kontakty:
ng g service contacts
Dzięki pomocy Angular CLI, nie musimy już powtarzać wielu manualnych czynności co znacznie przyspiesza pracę z Angular 2.
Określenie „serwis” dotyczy w zasadzie dowolnej klasy w Angularze, która nie jest komponentem. Na przykład modele, wszelkie klasy pomocnicze i pośredniczące.
Serwisy w Angular 2
Kiedy otwieram wygenerowany właśnie przy pomocy Angular CLI serwis, moim oczom ukazuje się podstawowy kod:
import { Injectable } from '@angular/core';
@Injectable()
export class ContactsService {
}
Jest to zwykła klasa z dodanym dekoratorem @Injectable
. Do czego jest potrzebny dekorator @Injectable
? Jeśli klasa, którą tworzymy wykorzystuje Dependency Injection to musi mieć jakiś dekorator, np. @Injectable
. Jest to wymaganie stawiane przez TypeScript – Angular 2 korzysta z metadanych parametrów przekazywanych do konstruktora, aby wstrzyknąć prawidłowe zależności. Te metadane są jednak nieobecne jeśli klasa nie ma żadnego dekoratora! W tym przypadku nie korzystamy (jeszcze) z Dependency Injection, jednak zalecam dodać dekorator ze względu na spójność z resztą aplikacji.
Dobrą praktyką jest dodawanie dekoratora @Injectable
do każdego serwisu.
Stworzony serwis ma za zadanie przechowywać tablicę kontaktów. Zaczynamy więc od zadeklarowania interfejsu reprezentującego kontakt, a następnie dodajemy do klasy serwisu odpowiednie pole z jednym kontaktem (przykładowo). Cała klasa serwisu wygląda tak:
import {Injectable} from '@angular/core';
export interface Contact {
id:number;
name:string;
age:number;
email:string;
}
@Injectable()
export class ContactsService {
contacts:Array<Contact> = [{
id: 1,
name: 'Tester',
age: 99,
email: 'randomemail@com.pl'
}];
}
Dependency Injection
Tak napisaną klasę ContactsService
możemy wstrzyknąć w dowolne miejsce w aplikacji. Dependency Injection w Angular 2 jest podobne do tego z AngularJS, ale znacznie bardziej rozbudowane i dające więcej możliwości.
Dependency Injection jest wzorcem projektowym, który ma na celu usuwanie ścisłych powiązań pomiędzy komponentami aplikacji. Jest to realizowane poprzez odwrócenie kontroli (Inversion of Control) – komponenty nie tworzą ani nie ładują potrzebnych serwisów, a zamiast tego definiują listę zależności i oczekują, że te zależności zostaną im przekazane. Rozwiązanie to jest niezwykle elastyczne i ułatwia tworzenie aplikacji. Więcej na temat teorii można poczytać choćby na wikipedii.
Jednym z nowych elementów DI w Angular 2 jest fakt, że równolegle do drzewa komponentów istnieje również drzewo hierarchicznych zależności, a konfiguracja Dependency Injection może nastąpić na dowolnym poziomie tego drzewa (w dowolnym komponencie). Więcej na ten temat można doczytać w dokumentacji. Na potrzeby tego wpisu wystarczy nam informacja, że aby zależność można było wstrzyknąć, musimy podać również który komponent ją udostępnia. Robimy to przy pomocy tablicy providers
w dekoratorze @Component
. Ponieważ chcemy, aby serwis był „globalny”, więc dodajemy tablicę providers
w najwyższym komponencie aplikacji. W tym przykładzie akurat do tego samego komponentu wstrzykujemy również instancję serwisu, aby skorzystać z tablicy kontaktów:
@Component({
…
providers: [ContactsService]
})
export class AppComponent {
constructor(private contactsService: ContactsService) {
}
}
Zdarzenia cyklu życia
Opisywałem już zdarzenia cyklu życia (lifecycle hooks) w AngularJS 1.5 i podobny koncept istnieje również w Angular 2. Krótko mówiąc, chodzi o takie metody w klasie komponentu, które są automatycznie wywoływane przez Angulara gdy komponent jest tworzony, zmieniany lub niszczony. Szczegóły można doczytać, w tym przypadku interesuje nas jedno konkretne zdarzenie: tworzenie komponentu.
Dobrą praktyką jest umieszczanie jak najmniej logiki w konstruktorze klasy. Dzięki temu instancjonowanie komponentu jest szybsze, łatwiej nim zarządzać i testować. Bardziej skomplikowane operacje zalecam przenieść do metody ngOnInit
z interfejsu OnInit
.
Aby wpiąć się w jedno ze zdarzeń cyklu życia, polecane jest zaimplementowanie w klasie komponentu odpowiedniego interfejsu udostępnianego przez Angulara. Chcemy wykonać pewne operacje od razu po stworzeniu komponentu, więc implementujemy interfejs OnInit
, który zawiera metodę ngOnInit
. Zostanie ona automatycznie wywołana przez Angulara po stworzeniu komponentu. Cały kod klasy ostatecznie wygląda w ten sposób:
export class AppComponent implements OnInit {
public contacts:Array<Contact>;
ngOnInit() {
this.contacts = this.contactsService.contacts;
}
constructor(private contactsService:ContactsService) {
}
}
Wejście i wyjście komponentu
Widok stworzonego komponentu zawiera w sobie tylko jeden komponent <typeofweb-contacts-list>
. Chcielibyśmy do tego komponentu przekazać jako argument tablicę z kontaktami. Jak to zrobić? Opisywałem wcześniej różne rodzaje bindingów w Angular 2. Można przekazać coś do komponentu, odebrać od niego, lub użyć bindingu dwukierunkowego. Służą do tego specjalne dekoratory @Input
i @Output
, które umieszcza się przed nazwą pola w klasie:
export class TypeofwebComponent {
@Input() name:string;
@Output() event = new EventEmitter();
}
Aby teraz przekazać do komponentu atrybut name
wystarczy po prostu napisać:
<typeofweb-component name="Michał"></typeofweb-component>
<typeofweb-component [name]="variable"></typeofweb-component>
W pierwszej wersji przekazywany jest ciąg znaków „Michał”, w drugiej przekazana zostanie zawartość zmiennej variable
. Skorzystanie z bindingu @Output
jest nieco bardziej skomplikowane. Przede wszystkim w kodzie HTML przekazujemy funkcję, którą definiuje się w klasie wyżej:
<typeofweb-component (event)=“handler($event)”></typeofweb-component>
Teraz, aby handler
został wywołany, konieczne jest wyemitowanie zdarzenia wewnątrz klasy TypeofwebComponent
:
this.event.emit('Dziala');
Nie wspomniałem tutaj o bindingu dwukierunkowym, gdyż nie ma do niego specjalnej składni. Taki binding definiuje się poprzez stworzenie pola z dekoratorem @Input
i pola o tej samej nazwie z suffiksem Change
z dekoratorem @Output
:
<typeofweb-component [(name)]=“variable”></typeofweb-component>
@Input() name:string;
@Output() nameChange = new EventEmitter();
Referencja na element
Przy okazji warto wspomnieć o jeszcze jednym specjalnym symbolu używanym w szablonach: #
czyli referencji na element. Spójrzmy od razu na przykład. Zakładam, że w klasie komponentu istnieje metoda log
, która wypisuje przekazaną wartość do konsoli:
<input #mojInput>
<button (click)="log(mojInput.value)">Log</button>
Teraz po wpisaniu czegoś w input i kliknięciu w guzik, w konsoli wyświetli się wpisana wartość. Jak to działa? #mojInput
oznacza stworzenie lokalnej (w szablonie) zmiennej, która wskazuje na dany element. Pod zmienną mojInput
znajduje się w tym przypadku referencja na input, a więc mojInput.value
zawiera w sobie wartość wpisaną w pole. Jeśli #
zostanie użyty na elemencie, który jest komponentem, to referencja będzie wskazywała na kontroler tego komponentu – więcej w dokumentacji.
Lista kontaktów
Wróćmy do tworzonej aplikacji. W komponencie ContactsListComponent
definiujemy pole przyjmujące tablicę kontaktów:
@Input() contacts:Array<Contact>;
a w widoku AppComponent
przekazujemy ją jako atrybut:
<typeofweb-contacts-list [contacts]="contacts"></typeofweb-contacts-list>
To jednak… nie działa. Jeszcze. Dodatkowo każdy komponent (lub jego rodzic, patrz punkt o hierarchicznym Dependency Injection) powinien definować komponenty i dyrektywy, z których będzie korzystał (analogicznie do tablicy providers
). Do dekoratora @Component
komponentu AppComponent
dodajemy więc:
directives: [ContactsListComponent]
Prawie gotowa aplikacja
Korzystając z wiedzy, którą już posiadamy, możemy teraz napisać resztę aplikacji. Po pierwsze definiujemy, że ContactsListComponent
będzie korzystał z ContactItemComponent
:
directives: [ContactItemComponent]
W widoku listy kontaktów iteruję po wszystkich kontaktach i kolejno je wyświetlam:
<ul>
<li *ngFor="let contact of contacts">
<typeofweb-contact-item [contact]="contact"></typeofweb-contact-item>
</li>
</ul>
Komponent przyjmuje jako atrybut pojedynczy obiekt z kontaktem:
@Input() contact:Contact;
I wyświetla go:
<div>Name: {{contact.name}}</div>
<div>Age: {{contact.age}}</div>
<typeofweb-gravatar [email]="contact.email" size="64"></typeofweb-gravatar>
Voilà! Gotowe. Źródła komponentu typeofweb-gravatar
tutaj pominę, ale całość dostępna jest na moim GitHubie: github.com/typeofweb/angular2-contacts-list. Efekt prezentuje się poniżej:
Podsumowanie
W tym artykule opisałem kilka istotnych elementów tworzenia aplikacji w Angular 2. Po pierwsze nowe komendy Angular CLI: ng generate …
. Ponadto omówiłem sposoby implementacji wejścia i wyjścia do komponentów, wstrzykiwanie zależności oraz tworzenie pośredniczących serwisów. W kolejnej części dokończę projekt listy zadań w Angular 2 i omówię komunikację pomiędzy komponentami z wykorzystaniem Reduksa. Aby oswoić się z samymi konceptami Reduksa, polecam mój wpis Flux i Redux. Zachęcam do komentowania!