Skocz do treści

Już wkrótce odpalamy zapisy na drugą edycję next13masters.pl. Zapisz się na listę oczekujących!

Wstęp do Angular 2

Obiecywałem i oto jest: Mój wstęp do tworzenia aplikacji w Angular 2. Chciałbym omówić tutaj podstawy nowego frameworka oraz narzędzia Angular CLI na przykładzie zbudowania od zera prostej Single Page Application – listy zadań. Jest to typowy, wielokrotnie powielany przykład używany do szybkiego poznania nowych front-endowych narzędzi.

Ten artykuł jest częścią 1 z 5 w serii Angular 2.

Zdjęcie Michał Miszczyszyn
JavaScript6 komentarzy

W tym kursie korzystam z TypeScript, więc jeśli jeszcze nie znasz tego języka to zapraszam do mojego kursu TypeScript:

https://typeofweb.com/typescript-czesc-1/

TypeScript – część 1

„TypeScript – typowany nadzbiór JavaScriptu, kompilowany do czystego JavaScriptu” – głosi napis na stronie głównej typescriptlang.org. Używam go praktycznie codziennie, w różnych projektach, z różnymi technologiami. Od pewnego czasu, w dużej mierze za sprawą Angulara 2, ale nie tylko, TS zaczął zyskiwać sporą popularność i uznanie w społeczności webdeveloperów.

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:

Angular 2 Todo List Hello World

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.

Angular 2 lokalne style

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
{{expression}}
[target]="expression"
bind-target="expression"  
        </td>
        <td>
        Interpolacja<br>
        Właściwość<br>
        Atrybut<br>
        Klasa<br>
        Style<br>
        </td>
    </tr>
    <tr>
        <td>Jednokierunkowy od widoku do źródła danych</td>
        <td>
(target)="statement"
on-target="statement"  
        </td>
        <td>
        Zdarzenie
        </td>
    </tr>
    <tr>
        <td>Dwukierunkowy</td>
        <td>
[(target)]="expression"
bindon-target="expression"  
        </td>
        <td>
        Dwukierunkowy
        </td>
    </tr>
</tbody>

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:

  1. 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.
  2. 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:

Angular 2 todo list

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.

👉  Znalazłeś/aś błąd?  👈Edytuj ten wpis na GitHubie!

Autor