PRA09.rst

Pracownia Programowania

Front End

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

Angular

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).

Typescript

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();

Instalacja w domu

Aplikacja wymaga zainstalowania node'a na komputerze:

Zestaw modułów i narzędzi

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

Błędy

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

Na wydziale uruchomienie

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/

Aplikacja

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.

Struktura projektu

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.

Model

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

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).

Service

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.

Komponenty

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'.

Moduły

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

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, *ngIf
atrybutowe (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ść $implicit
Do 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

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:

Odświeżanie

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