W tym kursie korzystam z TypeScript, więc jeśli jeszcze nie znasz tego języka to zapraszam do mojego kursu TypeScript:
Angular 2 RC
Na wstępie muszę jasno powiedzieć o tym, że Angular 2 nie jest jeszcze gotowy na produkcję. W momencie powstawania tego wpisu najnowszą wersją tego frameworka była Release Candidate 1. Jest to wersja prawie gotowa do wydania, jednak minie jeszcze trochę czasu zanim całość się ustabilizuje. I nie mam tutaj na myśli wyłącznie kodu źródłowego samego Angulara 2 – chodzi mi także o cały ekosystem, narzędzia, dobre praktyki itd.
Zespół Angulara pracuje teraz nad poprawą stabilności i zmniejszeniem liczby ładowanych plików oraz rozmiaru aplikacji opartych o Angular 2, gdyż na chwilę obecną prosty projekt typu Hello, world! na „dzień dobry” wykonuje 294 żądania oraz pobiera 594 kilobajty danych. To może nie mieć znaczenia w niektórych przypadkach (np. specjalistyczne aplikacje albo intranety), ale jednak warto pamiętać, że w tym momencie Angular 2 jest bardzo ciężki. Postępy prac można śledzić na github.com/angular/angular.
Angular 2: Pierwsze starcie
Powszechnym problemem przy pracy z AngularJS, szczególnie na początku cyklu życia tego frameworka, był brak dobrych wskazówek i powszechnych praktyk. Pisałem już o tym w innym wpisie: Struktura aplikacji AngularJS (część 1 ‑ trochę historii). Zespół Angulara 2 najwyraźniej wziął sobie do serca ten problem, gdyż od samego początku istnieją oficjalne dobre praktyki pisania aplikacji opartych o ten framework. Opisane jest w nich w zasadzie wszystko, od struktury folderów, przez nazewnictwo plików aż po formowatowanie. Zasady te zostały podzielone w 10 grup i ponumerowane, dzięki czemu łatwo się do nich odwołać na przykład w trakcie code review. Całość do przeczytania pod angular.io/styleguide.
Można zadać sobie pytanie dlaczego by nie pójść o krok dalej i całego procesu nie zautomatyzować? Istnieją przecież narzędzia, które pomagają generować pliki i foldery w odpowiedniej strukturze, testować kod pod kątem zgodności z pewnymi ustaleniami itd. Przykładem mogą być generatory do Yeomana lub bardzo specjalistyczne narzędzie do frameworka Ember: Ember CLI. Zespół Angulara również wpadł na podobnym pomysł i to właśnie na Ember CLI bazuje analogiczny „przybornik” dla Angulara: Angular CLI. Przy tworzeniu tej prostej aplikacji będę z niego korzystał.
Początek projektu
Rozpoczęcie nowego projektu opartego o Angular 2 wymaga stworzenia odpowiedniej struktury folderów, zainstalowania TypeScript oraz definicji typów, skonfigurowania środowiska i SystemJS, stworzenia pierwszego komponentu i wywołania funkcji bootstrap
. Jest to całkiem sporo zachodu, dlatego przy tym prostym projekcie zdecydowałem się skorzystać z Angular CLI. Zaczynam więc od jego instalacji, wymagany jest node.js w wersji 4 lub nowszej oraz npm:
npm install -g angular-cli
Angular CLI jest we wczesnej fazie rozwoju i dlatego komendy, ich działanie oraz konfiguracja projektów mogą ulec w przyszłości zmianie.
Angular CLI powinien być teraz zainstalowany i dostępny globalnie. Komenda to ng
, a nową aplikację tworzy się wywołując:
ng new todo_list
Przy pomocy tego jednego polecenia otrzymujemy gotowy projekt w Angular 2, razem z TypeScript, SystemJS i wieloma przydatnymi narzędziami oraz testami jednostkowymi i e2e. Automatycznie tworzone jest też repozytorium git oraz instalowane są pakiety npm. Fajne, prawda? :)
Aktualnie Angular CLI umożliwia tworzenie wyłącznie projektów opartych o TypeScript. W przyszłości zostanie dodane wsparcie również dla czystego JavaScriptu.
Aby uruchomić serwer wystarczy wydać polecenie ng serve
. Domyślnie aplikacja dostępna jest pod adresem http://localhost:4200/ Po jego otwarciu naszym oczom powinien ukazać się napis:
Aplikacja w Angular 2
Aby Angular zaczął działać należy stworzyć główny komponent aplikacji i przekazać go do funkcji bootstrap
. W wygenerowanym projekcie dzieje się to w pliku main.ts
:
import { bootstrap } from '@angular/platform-browser-dynamic';
import { TodoListAppComponent } from './app/';
bootstrap(TodoListAppComponent);
Stwórzmy więc komponent TodoListAppComponent
. Jego zadaniem będzie na razie wyłącznie wyświetlenie treści zmiennej title
wewnątrz nagłówka. Komponent w Angularze 2 jest zwykłą klasą poprzedzoną dekoratorem @Component
zaimportowanym z @angular/core
.
Jeśli koncept albo składnia dekoratorów jest Ci obca to polecam zapoznać się z oficjalną dokumentacją dekoratorów w TypeScript. Prawdopodobnie podobne adnotacje/dekoratory niedługo znajdą się w samym JavaScripcie, więc warto się z nimi oswoić już teraz – składnia w TypeScripcie bazuje na proponowanej składni do ECMAScript. Jednak na potrzeby wprowadzenia do Angulara 2 dogłębne studiowanie dokumentacji nie jest potrzebne.
Dekorator @Component
przyjmuje jako argument obiekt z konfiguracją. Na tę chwilę ważne są dla nas tylko dwa atrybuty: selector
oraz template
. Pierwszy z nich definiuje w jaki sposób będziemy identyfikować nasz komponent i ma składnię podobną do selektorów z CSS. Zapis 'todo-list-app'
oznacza, że komponent będzie dostępny jako nowy element HTML: <todo-list-app>
. Atrybut template
oczywiście definiuje szablon HTML dla naszego komponentu. Podobnie jak w AngularJS, aby wypisać tekst wewnątrz szablonu używamy {{
i }}
:
import { Component } from '@angular/core';
@Component({
selector: 'todo-list-app',
template: `<h1>{{ title }}</h1>`
})
export class TodoListAppComponent {
title = 'todo-list works!';
}
Tak zdefiniowany komponent może zostać użyty w pliku index.html
:
<todo-list-app></todo-list-app>
Szablon i style w osobnym pliku
Rzadko kiedy szablon HTML zawieram wewnątrz pliku .ts z komponentem. Myślę, że dobrym wzorcem jest wydzielanie styli oraz szablonów do osobnych plików. Angular 2 oczywiście również na to pozwala i dekorator @Component
zawiera odpowiednie atrybuty: templateUrl
i styleUrls
– pierwszy z nich przyjmuje ścieżkę do pliku, a drugi tablicę ścieżek, gdyż komponent może mieć tylko jeden szablon, ale wiele plików ze stylami. Warto tutaj zwrócić uwagę, że domyślnie te ścieżki muszą być bezwzględne. Przy naszej małej aplikacji nie stanowi to najmniejszego kłopotu, jednak przy większych projektach może rodzić pewne problemy. Rozwiązaniem jest wykorzystanie atrybutu moduleId
, można o tym przeczytać w dokumentacji.
Koniecznie muszę wspomnieć o tym, że style podane jako atrybut do komponentu są lokalne. Oznacza to, że dodanie do stworzonego komponentu styli jak poniżej nie spowoduje, że wszystkie elementy H1 na stronie będą czerwone:
@Component({
…
styles: ['h1 { color: red; }']
})
Angular 2 wkłada trochę wysiłku w to, aby style działały wyłącznie per-komponent. Jak to się dzieje? Jeśli teraz przyjrzymy się wygenerowanemu HTML-owi to zauważymy, że Angular 2 automatycznie dodaje dziwnie wyglądające atrybuty do elementów w aplikacji i te same atrybuty dopisywane są również do treści stylów. Sposób generowania nazw tych atrybutów nie jest w tym momencie istotny, liczy się efekt: style stają się niejako lokalne dla komponentu.
Lista zadań
Przejdźmy więc do konkretów: Lista zadań w Angular 2. Na potrzeby tego wpisu pełną implementację umieszczę w jednym komponencie. To jest prawdopodobnie coś, czego nie chcesz robić w prawdziwych aplikacjach i pisałem już o tym we wpisie Struktura aplikacji AngularJS (część 2 ‑ komponenty). Więcej o podziale na komponenty oraz o komunikacji pomiędzy nimi z użyciem Redux w Angular 2 napiszę w kolejnym wpisie.
Prosta lista zadań powinna zawierać input oraz listę z zadaniami. Do tego powinna być możliwość oznaczenia każdego z zadań jako wykonane - wystarczy prosty checkbox. HTML może wyglądać w następujący sposób:
<h1>Lista zadań</h1>
<label>
Nowe zadanie: <input type="text">
</label>
<ul>
<li>
<input type="checkbox"> Moje zadanie
</li>
</ul>
Bindingi
Osoby znające AngularJS napewno są zaprzyjaźnione z two-way data binding i dyrektywą ng-model
. Jednak powszechnym problemem była mała czytelność szablonów HTML – zapis <moja-dyrektywa atrybut=“costam”>
mogła oznaczać zarówno binding dwukierunkowy pomiędzy atrybut
a costam
, jak i przypisanie do atrybut
ciągu znaków ”costam"
. Nie było to jasne na podstawie samego szablonu i aby się upewnić należało zajrzeć do kodu źródłowego dyrektywy, i sprawdzić czy atrybut
został zadeklarowany jako @
czy jako =
.
W Angularze 2 koncept bindingów został mocno rozbudowany, ale jednocześnie stał się też bardziej klarowny. Możliwe jest teraz zdefiniowanie różnych bindingów używając składni w HTML. Bindingi można podzielić na trzy kategorie1:
Kierunek | Składnia | Typ bindingu |
---|---|---|
Jednokierunkowy od źródła danych do widoku |
|
Jak widzimy, bindingi w Angular 2 otoczone są []
lub ()
, albo poprzedzone jednym z prefiksów bind-
, on-
lub bindon-
. Zarówno użycie znaków interpunkcyjnych jak i prefiksów jest sobie równoważne i zależy wyłącznie od preferencji programisty. Dzięki odróżnieniu od siebie bindingów, składnia szablonów staje się bardziej czytelna.
Interakcja z elementami
Dość teorii! Przejdźmy do praktyki. Chcemy, aby tekst w inpucie był zsynchronizowany z polem w naszej klasie. Możemy to zrobić na dwa sposoby:
- Używamy dwóch bindingów. Jeden z nich to synchronizacja atrybutu w klasie do wartości w inpucie, a drugi to podpięcie się pod zdarzenie
input
i aktualizacja wartości w klasie gdy wpisujemy coś w inpucie. - Użycie bindingu dwukierunkowego (który tak naprawdę jest dwoma bindigami; stąd jego składnia to połączenie obu rodzajów bindingów).
Druga opcja wydaje się być prostsza i rzeczywiście - w przypadku edycji formularzy binding dwukierunkowy jest niezwykle przydatny. Użyję tutaj specjalnej dyrektywy dostarczanej przez framework – ngModel
. Oprócz synchronizacji umożliwia ona także walidację pola oraz obsługę błędów, ale więcej na ten temat można doczytać w dokumentacji. Modyfikuję więc klasę i szablon:
export class TodoListAppComponent {
newTodoTitle:string = '';
}
<input [(ngModel)]="newTodoTitle">
I już! Od teraz zawartość inputa oraz wartość pola newTodoTitle
w klasie komponentu będą zsynchronizowane.
Następnym krokiem jest sprawienie, aby po wciśnięciu klawisza ENTER zostało stworzone nowe zadanie o wpisanym tytule, a input stał się pusty. Użyjemy do tego bindingu do zdarzenia keyup
, które jest wywoływane zawsze gdy wciśnięty (a właściwie to puszczony) zostaje jakiś klawisz na klawiaturze. Aby wykrywać tylko wciśnięcie klawisza ENTER, musielibyśmy przefiltrować wszystkie te zdarzenia (np. sprawdzając kod klawisza w event.which
). Okazuje się jednak, że jest to na tyle częsta potrzeba, iż Angular 2 zawiera w sobie skróconą składnię umożliwającą prostsze wykrywanie naciśnięć niektórych klawiszy: keyup.enter
.
<input … (keyup.enter)="addTodo()">
export class TodoListAppComponent {
…
addTodo() {
console.log(this.newTodoTitle);
}
}
Jeśli teraz wpiszemy coś w pole i wciśniemy ENTER to w konsoli powinien pojawić się wpisany tekst.
Wyświetlanie elementów
Kolejnym krokiem będzie stworzenie tablicy przechowującej nasze elementy z listy zadań oraz wyświetlenie tej listy pod inputem. Bez dalszej zwłoki, przejdźmy od razu do kodu:
interface Todo {
title:string;
complete:boolean;
}
export class TodoListAppComponent {
todos:Array<Todo> = [];
}
Najpierw definiuję interfejs Todo
, a następnie pole będące tablicą tychże zadań. Początkowo tablica jest pusta, ale do testów możemy ręcznie dodać do niej kilka elementów:
todos:Array<Todo> = [{
title: 'kupić chleb',
complete: true
}, {
title: 'zrobić kanapkę',
complete: false
}];
Następnie wyświetlimy te elementy na stronie. Posłuży nam do tego dyrektywa ngFor
dostarczona przez framework:
<li *ngFor="let todo of todos">
<input type="checkbox" [(ngModel)]="todo.complete">
{{ todo.title }}
</li>
Użyta tutaj składnia let todo of todos
przypomina składnię pętli for-of
w ECMAScript i na niej była bazowana. Oko przykuwa jednak inny drobny szczegół – zamiast ngFor
napisałem *ngFor
. To bardzo ważne! Dyrektywa ngFor
jest jedną z niewielu tzw. dyrektyw strukturalnych, które potrafią dodawać i usuwać elementy z drzewa DOM. Zapis *ngFor
jest tak naprawdę uproszczeniem pełnej składni, która w tym przypadku wyglądałaby tak:
<template ngFor let-todo [ngForOf]="todos">
<li>
<input type="checkbox" [(ngModel)]="todo.complete">
{{ todo.title }}
</li>
</template>
Trzeba przyznać, że uproszczenie to jest znaczące :)
Nie pozostaje już teraz nic innego niż dopisać ciało metody addTodo
. Najpierw sprawdzam czy został wpisany już jakiś tekst – jeśli nie to nic nie robię. Następnie tworzę nową zmienną typu Todo
i przypisuję do niej wpisany tytuł oraz ustawiam domyślnie, że zadanie nie zostało jeszcze wykonane. Na koniec dodaję nowy element do tablicy i czyszczę input:
addTodo() {
if (!this.newTodoTitle) {
return;
}
const newTodo:Todo = {
title: this.newTodoTitle,
complete: false
};
this.todos.push(newTodo);
this.newTodoTitle = '';
}
Prawie efekt końcowy…
Aplikacja już prawie gotowa. Jest piękna:
Na tym zakończę ten wpis. Nauczyliśmy się jak stworzyć prosty projekt oparty o framework Angular 2 i TypeScript. Omówiłem pobieżnie rodzaje bindingów w nowym angularze i sposób tworzenia komponentów. Opisałem też podstawy obsługi interakcji użytkownika z aplikacją. Zachęcam do komentowania :)
Podstawy są, pozostaje jednak kilka problemów. Po pierwsze pomieszałem trochę odpowiedzialności – komponent teraz przechowuje listę zadań, odpowiada za ich edycję, dodawanie i wyświetlanie. Nie powinno tak być, dlatego fragment aplikacji należy wydzielić do osobnego serwisu. Dodatkowo cała aplikacja to tak naprawdę tylko jeden komponent. To działa w tak prostym przypadku, ale nie jest do końca zgodne ze sztuką – dlatego stworzoną listę zadań powinienem podzielić na mniejsze komponenty, a komunikację pomiędzy nimi zaimplementować przy pomocy pośredniczącego serwisu, np. używając Redux. To wszystko postaram się zrobić w kolejnym wpisie.