Uwaga! cały kod znajduje się na gałęzi w repozytorium https://github.com/WitMar/PRA2018-2019/tree/Angular na gałęzi Angular. Końcowy kod znajduje się na https://github.com/WitMar/PRA2018-2019/tree/AngularFinal
Autor kodu : Michał Kurkowski
AngularJS - otwarta biblioteka framework języka JavaScript, wspierana i firmowana przez Google, wspomagająca tworzenie i rozwój aplikacji internetowych na pojedynczej stronie (single page application). Zadaniem biblioteki jest wdrożenie wzorca Model-View-Controller (MVC) do aplikacji internetowych, aby ułatwić ich rozwój i testowanie.
Główną różnicą pomiędzy modelem Single Page Application (SPA), a standardowymi aplikacjami jest jednostronicowy interfejs oraz przeniesienie logiki z serwera na klienta. Cała logika aplikacji jest napisana po stronie klienta w JavaScript i wykonywana w przeglądarce. Kod HTML, JavaScript i CSS jest pobierany jednorazowo w trakcie uruchomienia aplikacji, natomiast pozostałe wymagane zasoby zostaną pobrane dynamicznie, gdy będą potrzebne w danej chwili.
Aby stworzyć nowy projekt najłatwiej jest użyć do tego Angular CLI (command line interface) postępując zgodnie z instrukacjami podanymi na stronie Angular4 QuickStart:
Będziesz potrzebować NodeJS w wersji minimum 6.9.x oraz npm minimum 3.x.x.
Node.js jest środowiskiem programistycznym zaprojektowanym do tworzenia wysoce skalowalnych aplikacji internetowych, szczególnie serwerów www napisanych w języku JavaScript. Node.js umożliwia tworzenie aplikacji sterowanych zdarzeniami wykorzystujących asynchroniczny system wejścia-wyjścia.
W systemach typu Linux wyszukaj pakietów nodejs i npm, np. instalacja dla systemu Ubuntu wygląda następująco:
sudo apt-get install nodejs npm
Środowiskiem pracy z javascriptem może być dowolny edytor tekstu lub bardziej zaawansowane środowisko jak np. bardzo podobny do IntelliJ (jest bardzo pomocny w wyszukiwaniu błędów składniowych i syntaktycznych w kodzie).
Nowy Angular4 jest napisany w TypeScript. TypeScript jest rozbudowanym JavaScriptem, transpilowanym (kompilacja kodu źródłowego w inny kod źródłowy) do JavaScript. Zawiera to samo co JavaScript oraz trochę dodatków, posiada między innymi silne typowanie, interfejsy i programowanie obiektowe oparte na klasach (konstruktory, dziedziczenie). Jego popularność rośnie wraz z wzrostem złożoności webowych aplikacji. Możesz poznać działanie TypeScript zaczynając od strony TypeScript Playground:
omówienie po Polsku:
Czym jest silne typowanie? Jest wskazaniem jakiego typu dane mają być przechowywane pod daną zmienną. Dzięki temu już na etapie kompilacji można wychwycić podstawowe błędy. W JavaScripcie istnieją następujące typy: string, number, boolean, object, function, undefined. Szczególnie istotne jest wskazanie typów parametrów w deklaracjach funkcji. Pamiętaj, że do atrybutów klasy musisz odwoływać się za każdym razem podając
this.nazwa_zmiennej
Zmienne definiujemy poprzez słowo kluczowe let. Jeżeli nie chcemy dopuścić zmian wartości zmiennej możemy zamiast let skorzystać ze słowa kluczowego const. Zmienne zdefiniowane przez let są widoczne w ramach bloku, w których je zdefiniowaliśmy. Przykłady kodu:
//variables
let name : string = 'Jan';
let decimal: number = 61;
let cyfry: number[] = [1, 2, 3];
let t: [string, number];
t = ['Jan', 24]; // Poprawne
//interfaces
interface Message {
text: string;
}
function showAlert(msg: Message) {
console.log(msg.text);
}
showAlert({text : 'No data!'});
//classes
class Employee {
name: string;
constructor(name: string) {
this.name = name;
}
fire() {
alert('You are fired, ' + this.name);
}
}
let employee = new Employee("Jan");
employee.fire();
Następnie z poziomu terminala w katalogu projektu należy wywołać następujące polecenia:
npm install @angular/cli
// instaluje cli angulara, narzędzie ulatwiające pracę z angularem
npm install json-server
// instaluje crudowy serwer opierający się o plik *.json
npm install
// (z poziomu root aplikacji) instaluje niezbędne moduły do uruchomienia aplikacji
Jeżeli widzę błąd:
npm ERR! code ENOTFOUND
npm ERR! errno ENOTFOUND
npm ERR! network request to https://registry.npmjs.org/inline-process-browser/-/inline-process-browser-1.0.0.tgz failed, reason: getaddrinfo ENOTFOUND registry.npmjs.org registry.npmjs.org:443
npm ERR! network This is a problem related to network connectivity.
npm ERR! network In most cases you are behind a proxy or have bad network settings.
npm ERR! network
npm ERR! network If you are behind a proxy, please make sure that the
npm ERR! network 'proxy' config is set properly. See: 'npm help config'
Spróbuj wykonać jedno z następujących rzeczy:
npm config set registry http://registry.npmjs.org
W ostateczności(!): add 104.16.20.35 registry.npmjs.org to /etc/hosts file
Trzeba zainstalować pakiety i ustawić ścieżkę w path
P:
mkdir s[numer_indeksu]
cd s[numer_indeksu]
npm install @angular/cli
npm install json-server
cd node_modules/.bin/
Uruchomienie serwera wchodzisz do folderu, w którym znajduje się projekt i wywołujesz polecenie:
json-server --watch db.json
Uruchomienia aplikacji : wchodzisz do folderu, w którym znajduje się projekt i wywołujesz polecenie:
ng serve
Aplikacja dostępna będzie pod localhost:4200, server pod localhost:3000.
Aplikacja buduje się na żywo, podczas każdej zmiany w plikach źródłowych. Wystarczy zapisać plik i od razu możemy zaobserwować wyniki zmian.
Główny katalog projektu, ważniejsze pliki:
.angular.json – główny plik konfiguracyjny projektu,.gitignore – mówi o tym, które pliki nie powinny wylądować w repozytorium (nie chcesz mieć tam choćby folderu node_modules czy też plików konfiguracyjnych dla IDE, gdy każdy może korzystać z innego),package.json – zbiór wszystkich paczek, zależności wykorzystywanych w projekcie.
Katalogi i pliki:
src – pliki źródłowe dla naszego projektu,index.html - domyślny plik ze stroną HTML,main.ts – inicjalizacja typescripta,styles.css – pierwszy plik stylów dla naszej aplikacji początkowej,favicon.ico – ikonka aplikacji, która pojawi się w zakładce (tab) przeglądarki (obok nazwy),polyfills.ts – przeglądarki różnią się między sobą. Ta sama funkcja użyta w jednej może nie działać w innej bądź też działać inaczej. Polyfills to takie kody, które implementują „braki” w przeglądarkach.
Serce projektu znajdziemy w podkatalogu:
app/ – tu znajdzie się tak naprawdę nasza aplikacja, wszystkie komponenty, widoki. Interesować nas będzie tylko ten katalog.app.module.ts – najważniejszy plik naszej aplikacji, tutaj załączasz wszystkie komponenty, moduły, serwisy, składające się na aplikację (odpowiednio w: declarations, imports oraz providers). Korzystając z Angular CLI przy ich tworzeniu – automatycznie są dodawane do tego pliku.
Nowo stworzone serwisy i komponenty nie są standardowo podpinane pod żaden moduł. Można to jednak łatwo zrobić, np. podpinając w głównym module aplikacji, w pliku src/app/app.module.ts.
U nas zdefiniowany jest w katalogu src/app/models.
Znak zapytania przy parametrze oznacza, że jest on opcjonalny. W kodzie sprawdzamy, czy przekazano obiekt i czy ma on pola id, name, finished.
Reszta składni jest podobna do Javy z tą różnicą, że typy definiujemy po dwukropku a nie przed nazwą zmiennej / metody.
constructor(obj?: any) {
this.id = (obj && obj.id) || Math.floor(Math.random() * 1000);
this.name = (obj && obj.name) || '';
this.finished = (obj && obj.finished) || false;
}
Musimy zdefiniować ten sam model co w serwerze backendowym.
Widok definiowany jest w klasie html. Główny plik to index.html, w którym my osadzamy komponent app-root zdefiniowany w app.component.
Jeżeli w naszej klasie w pliku ts zdefiniujemy jakąś zmienną, to możemy odwołać się do jej wartości w pliku html poprzez użycie
{{ title }}
Jak wspomniano wyżej strony budujemy z tagów HTML-owych oraz tagów komponentów (nazw z selectorów komponentów - patrz niżej).
Serwis zdefiniowany w pliku /service/todo.service.ts odpowiedzialny jest za ściąganie danych z serwera i ich obrabianie (np. sortowanie).
Odwołuje się on bezpośrednio do metod REST API serwera.
W ramach projektu klasa serwisu jest singletonem (tzn. inicjalizowana jest jedna jego instancja). Service jest przekazywany do komponentów poprzez mechanizm wstrzykiwania zależności, przez definicję w konstruktorze komponentu. Od strony serwisu mamy adnotację @Injectable() przed definicją klasy.
@Injectable()
export class TodoService {
constructor(private http: HttpClient) {}
}
W naszym przypadku definiujemy serwis dodając do niego moduł z biblioteki @angular/common/http do zapytań http. Na takim obiekcie możemy wykonywać zapytania HTTP, podając metodę i ścieżkę do wywołania.
this.http.delete(`${apiUrl}/todos/${todo.getId()}`);
Jako drugi parametr zapytania możemy przekazać ciało zapytania http.
this.http.post(`${apiUrl}/todos`, todo);
Kolejne parametry zależą od zapytania i zawierają takie rzeczy jak typ odpowiedzi, parametry itp. Możemy dodać klamrę i podać tylko wybrane parametry. Na przykład przekazywanie nagłówka http:
let headers = new HttpHeaders();
headers = headers.set('h1', 'v1').set('h2','v2');
http.get('someurl',{
headers: {'header1':'value1','header2':'value2'}
});
Na zapytaniu możemy wykonywać kolejne operacje na zasadzie przetwarzania "strumieniowego". Zapytanie HTTP zwraca typ Observable służący jako kolekcja do przechowywania wartości we wzorcu Obserwator (czyli dynamicznie aktualizowanych). Aby odzyskać dane z typu observable musimy się w nim "zarejestrować", czyli wykonać na nim metodę subscribe(). W metodzie tej definiujemy zachowanie wartości. Wynik zapytania dostępny jest w zmiennej res.
this.http.post(`${apiUrl}/todos`, todo).subscribe(res => {
console.log(res);
Kod z repozytorium służący do dodawania elementu do listy możemy przepisać na pętle for postaci (zastępując strumieniową funkcję map pętlą):
private getTodos() {
this.todoService.getAllTodos().subscribe(res => {
for (let i = 0; i < res.length; i++) {
this.todos.push(new Todo(res[i]));
}
});
}
Na subscribe mamy paremetr res - odpowiedz i parametr z błędem error - jeżeli zapytanie się nie powiedzie. Możemy ten drugi wypisać do konsoli żeby widzieć błędy (albo obłużyć je w jakiś inny sposób).
private getTodos() {
this.todoService.getAllTodos().subscribe(res => {
for (let i = 0; i < res.length; i++) {
this.todos.push(new Todo(res[i]));
}
},
error => {
console.log(error);
window.alert('Błąd');
}
);
}
Warto dzielić serwisy między różne funkcje, które mają pełnić, tzn. osobny serwis do logowania, zarządzania pracownikami, zarządzania planem itp.
Podstawowym składnikiem projektów w Angular4 są kompontenty. Kompontent kontroluje fragment strony. Po stworzeniu projektu Angular CLI inicjalizowany jest główny komponent: app.component, który kontroluje i zawiera wszystkie pozostałe komponenty. Każdy komponent ma swoją klasę, stronę html i styl css.
Pojedynczy komponent będzie składał się z:
app.component.ts – odpowiada za logikę komponentu, jego działanie,app.component.html – przechowuje jego strukturę,app.component.css – a tu go stylujemy.app.component.specs.ts – plik z testami komponentu (opcjonalnie).
W pliku *.ts górna cześć komponentu nazywana jest dekoratorem komponentu. To tutaj wskazane jest miejsce (element drzewa DOM) renderowania kompontentu (selector), wygląd komponentu (templateUrl) oraz jego style (styleUrls).
Nazwa z selektora jest także wykorzystywana przy odwołaniach się do danego komponentu z innego komponentu. Spójrz na przykład na plik index.html, który zawiera odwołanie do komponentu app-root. App-root jest głównym komponentem naszej Single Page Application.
Poniżej podany jest przykład klasy komponentu, konstruktor jest w tej klasie opcjonalny.
export class AddTodoComponent implements OnInit {
newTodoName: String;
constructor(private todoService: TodoService) {}
ngOnInit() {
this.newTodoName = '';
}
Komponent List przyjmuje w konstruktorze Service, który jest wstrzykiwany automatycznie.
Komponent List-Item przyjmuje w konstruktorze Service, ma także zdefiniowane dwa elementy przekazywany typ wejściowy i typ zwracany (podobnie do funkcji). Służą one do komunikacji między komponentami.
@Input() todo: Todo;
@Output() removeItem: EventEmitter<Todo> = new EventEmitter();
Od Angular 2 komponent rodzica ma możliwość przekazania do dziecka danych, które mogą determinować zachowanie komponentu lub w ogóle – pozwolić na jego odpowiednie wyrenderowanie. Jest to możliwe dzięki adnotacji @Input(). Możemy definiować im wartości domyślne jeżeli zainicjalizujemy je przy definicji.
Przekazanie parametru odbywa się poprzez adnotacje w HTML, podobnie jak wartości parametrów tagów HTML:
<nazwa_komponentu nazwa_zmiennej="wartosc"></nazwa_komponentu>
Adnotacja @Output() pozwala nam osiągnąć odwrotny rezultat – przekazać wartości od dziecka do rodzica – przy pomocy zdarzenia.
@Output() nazwa_zmiennej = new EventEmitter<string>();
EventEmitter jest klasą generyczną – szablonem. Powyżej określiliśmy, że parametr jaki będzie przekazywał w postaci argumentu, będzie typu string. Teraz, aby przekazać metodę, która wywoła się przy emisji zdarzenia, wystarczy poniższy zapis:
<nazwa_komponentu (nazwa_zmiennej)="nazwa_funkcji_rodzica($event)"></nazwa_komponentu>
Aby wyemitować zdarzenie, musimy wywołać metodę emit() obiektu nazwa_zmiennej:
this.nazwa_zmiennej.emit('abc');
Poskutkuje to wywołaniem funkcji nazwa_funkcji_rodzica (czyli komponentu w którym definiujemy nasz podkomponent) i przekazaniem mu w postaci argumentu – wartości 'abc'.
Modules (pol. Moduły) pozwalają nam podzielić aplikację na mniejsze łatwiejsze w zarządzaniu fragmenty (opakowują serwisy i moduł tak jak pakiety w Javie). W naszym projekcie mamy tylko jeden moduł.
Tu definiujemy ścieżki do komponentów (routing), definiujemy nowe serwisy i komponenty.
const ROUTES: Routes = [
{ path: '', redirectTo: 'todo-list', pathMatch: 'full' },
{ path: 'todo-list', component: TodoListComponent },
{ path: 'add-todo', component: AddTodoComponent }
];
@NgModule({
declarations: [AppComponent, TodoListComponent, TodoItemComponent, AddTodoComponent],
imports: [BrowserModule, HttpClientModule, FormsModule, RouterModule.forRoot(ROUTES)],
providers: [TodoService],
bootstrap: [AppComponent]
})
export class AppModule {}
W const ROUTES definiujemy odwzorowanie ze scieżek domenowych na componenty, parametr to tablica routingu. Pierwszy redirect podaje, że jak nic nie wpiszemy na stronie (pusta ścieżka) to idziemy do todo-list. Nazwy ścieżek wykorzystujemy także przy definiowaniu przekierowań w plikach html.
Dyrektywy w Angular4 (ang. Angular4 Directives) są sposobem dodawania dynamicznego zachowania do HTMLa. Dyrektywy są najczęściej wykorzystywanym elementem AngularJS i stanowią o jego sile i przewadze nad innymi frameworkami JavaScript.
Typy dyrektyw:
komponent (ang. Compontent) – jest dyrektywą z templatką.strukturalne (ang. Structural) – zmieniają wygląd dodając, usuwając lub zmieniając elementy HTML np: *ngFor, *ngIfatrybutowe (ang. Attribute)- zmieniają wygląd lub zachowanie elementu, komponentu albo innej dyrektywy
Dyrektywy tworzone mogą być na kilka różnych sposobów. Posiadają także szereg parametrów konfiguracyjnych, które odpowiadają za samo funkcjonowanie oraz użycie dyrektyw.
Angular musi wiedzieć gdzie zaczyna się nasza aplikacji służy do tego dyrektywa ng-app. Wstawiamy ją jako atrybut do elementu który będzie naszą aplikacją. W naszym przypadku może to być tag <html> lub <body>
Wyrażenie {{ }} zwany inaczej ngBind jest jedną z dyrektyw Angular. Jej celem jest wiązanie danych.
Zadanie
Dodaj dyrektywę ngModule do pola input formularza.
Aby dodać tę dyrektywę musisz zaimportować nowy moduł. Przejdź do app.module.ts.
Dodaj :
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
oraz dodaj wpis w imports
imports: [BrowserModule, HttpClientModule, FormsModule, RouterModule.forRoot(ROUTES), ReactiveFormsModule],
Dyrektywa ng-model wiąże zawartość pola tekstowego z tą w komponencie.
Relacja jest dwukierunkowa. Oznacza to, że jeśli zmienię zawartość pola tekstowego to zmienię zawartość username. Jeśli zmienię wartość username, zmienię także wartość tego pola tekstowego.
Dodaj zmienną name w todo-list.components.ts.
name: String;
Dodaj w todo-list.components.html:
<div>
Name: <input [(ngModel)]="name">
<h1>You entered: {{name}}</h1>
</div>
Strukturalne dyrektywy odpowiedzialne są za widok HTML. Kształtują one ten widok poprzez dodawania, usuwanie bądź manipulowanie obiektami DOM. Strukturalne dyrektywy są łatwe do rozpoznania – poprzedzone są znakiem *, tak jak na przykładzie.
ngIf jest najprostszą i najłatwiejszą do zrozumienia strukturalną dyrektywą. Bierze ona jakieś logiczne wyrażenie i sprawia, że cały blok kodu na którym została użyta pojawia się lub znika w zależności od wartości tego wyrażenia.
<p *ngIf="true">
Wyrażenie daje true.
Paragraf jest widoczny.
</p>
<p *ngIf="false">
Wyrażenie daje false.
Paragraf nie jest widoczny.
</p>
ng-For służy do iterowania po kolekcji.
<li *ngFor="let item of items; index as i; even as isEven; odd as isOdd; first as isFirst; last as isLast; trackBy: trackByFn">
({{i}}) {{item.name}}
</li>
Gdzie:
Słowo kluczowe let deklaruje zmienną do której możemy odnosić się wewnątrz elementu.Kiedy ngFor iteruje przez listę, ustawia i zmienia właściwości w swoim własnym kontekstowym obiekcie. Właściwości te to index i odd oraz specjalna właściwość $implicitDo zmiennych i i odd angular wstawia do nich wartości właściwości z kontekstowego obiektu.Właściwość let-item nie została ustawiona. Angular ustawia wartość let-item z $implicit które ngFor wykorzystuje podczas iteracji
Przykład, zamień kod z pliku todo-list.component.html na następujący:
<table class="table table-bordered table-striped">
<tr>
<tr *ngFor="let todoski of todos index as i; even as isEven; odd as isOdd">
<td>
<div [ngClass]="{'todo__name--finished' : todoski.isFinished() }"><font color="blue" *ngIf="isEven">{{i + 1}}
{{todoski.getName()}} </font></div>
<div [ngClass]="{'todo__name--finished' : todoski.isFinished() }"><font color="red" *ngIf="isOdd">{{i + 1}}
{{todoski.getName()}} </font></div>
</td>
<td>
<input [(ngModel)]="todoski.name">
</td>
<td>
<button (click)=removeTodo(todoski)>Remove</button>
</td>
<td>
<button (click)=save(todoski)>Save</button>
</td>
<button class="todo__btn todo__btn--status" (click)="toggleStatus(todoski)"> TOGGLE STATUS</button>
</tr>
</table>
Pozwala Ci on edytować wartości pól i usuwać, bez wykorzystania elementów todo-item.
Zadanie
Oprogramuj przycisk "Zapisz" kopiując odpowiednie elementy (funkcje) z przycisku Remove.
Zmień metodę updateTodo (zauważ, że wcześniej subscribe był w częsci item dopisany) na
updateTodo(todo: Todo) {
return this.http.put(`${apiUrl}/todos/${todo.getId()}`, todo).subscribe(res => {
console.log(res);
});
}
Uwaga! Możesz użyć tylko jednej strukturalnej dyrektywy na konkretnym elemencie!
Dyrektywa ngClass pozwala dynamicznie zmieniać CSS-yw zależności od prawdziwości wyrażenia podanego po dwukropku.
<div class="todo__name" [ngClass]="{'todo__name--finished' : todo.isFinished() }">
Filtry w Angular4 (ang. Angular4 Pipes) służą do zamiany danych wejściowych w pożądany format wyjściowy. Poniżej jest przykład transformacji tytułu do wielkich liter:
<h1>{{title | uppercase}}</h1>
Lista filtrów:
Zauważmy, że przełączając się miedzy ToDo List a Add Todo angular zmienia widok przeładowując tylko odpowiednie komponenty strony.
Zadanie
Dodaj w Postmanie nowy ToDo wykonując POST na http://localhost:3000/Todos a w body przekazując
{
"id": 111111222,
"name": "NOWY POST",
"finished": false
}
Przełącz się między widokiem listy a widokiem dodawania elementu, zobaczy czy na liście pojawi się nowy element.
Zadanie
Dodaj do modelu datę posta i zapisz w niej datę aktualną.
private date: Date;
constructor(obj?: any) {
this.id = (obj && obj.id) || Math.floor(Math.random() * 1000);
this.name = (obj && obj.name) || '';
this.finished = (obj && obj.finished) || false;
this.date = new Date(Date.now());
}
Dodaj wyświetlanie daty w widoku listy notatek.
Zauważ, że daty są takie same dla różnych notatek, gdyż przy przełączeniu na widok listy za każdym razem tworzone są notatki.
W koncowym kodzie dodano także obiekt Pipe, słuzący do filtrowania elementów tabeli.
Wykorzystano materiały z:
https://farmastron.pl/nauka-angular4-angular2-czesc-1/
http://10clouds.github.io/acodemy.io/intro/angular/
https://www.nettecode.com/angular-cli-struktura-projektu/
https://www.nafrontendzie.pl/podstawy-angularjs-niezbedne-minimum
http://eluzive.pl/2017/12/10/kurs-angular-14-strukturalne-dyrektywy/
https://kamilmysliwiec.com/kurs-angular-2-komunikacja-input-output