Angular w przykładach
wprowadzenie do frameworku
opracował Jerzy Wawro na podstawie publikacji open source
Angular w pigułce
Na początek krótko przedstawimy najważniejsze struktury i funkcje Angulara, które są prezentowane w tej książce. Ta prezentacja może służyć jako przewodnik pozwalający wybrać rozdziały, które pozwolą na uzupełnienie wiedzy, a pominąć te zagadnienia, które już są znane.
CLI
ng new [nazwa]
– generowanie nowej aplikacji
ng serve
- uruchomienie próbnena porcie 4200 (http://localhost:4200); można zmienić port, np: ng serve --port = 8080
ng build
- buduje aplikację w katalogu dist/ .
Generowanie elementów aplikacji:
- ng g component [nazwa] - Komponent
- ng g directive [nazwa] - Dyrektywa:
- ng g pipe [nazwa] - Potok
- ng g service [nazwa] - Usługa
- ng g class [nazwa] - Klasa
- ng g route [nazwa] - Trasa
Struktura aplikacji
Zgodnie z przyjętą konwencją, główny komponent aplikacji zapisywany jest w pliku: app/app.component.ts
.Opisuje on główny komponent aplikacji (zazwyczaj AppComponent), renderowany w elemencie DOM oznaczonym jako app-root (to też tylko konwencja).
Pozostałe pliki:
app/app.module.ts
– struktura modułuindex.html
- strona, na której będzie renderowany komponentmain.ts
- główny plik uruchomieniowy - łączący komponent ze stroną, zależy od platformy
AppComponent jako komponent początkowy (bootstrap) jest renderowany na każdym napotkanym elemencie HTML app-root . Dlatego w pliku index.html umieszczamy element app-root
.
Usługi i wstrzykiwanie zależności
Usługi to funkcje udostępniane przez wstrzykiwalnych „dostawców usług”:
@Injectable()
export class DostawcaUslugi{
usluga() {}
Do komponentów przekazywana w parametrach konstruktora:
@Component({
selector:'app-root',
template:'...'
})
export class AppComponent {
constructor(private uslugi: DostawcaUslug)
W modułach deklarujemy dostawców usług w sekcji providers – wówczas ich wstrzykiwani odbywa się automatycznie:
@NgModule({
providers:[DostawcaUslug],
Tworzenie komponentów
Komponent to klasa z dekoratorem TypeScript @Component
. Klasa zawiera logikę (Javascript) a dekorator opis widoku (template).
W dekoratorze podajemy metadane (minimum):
- selector - nazwa elementu DOM w którym będzie komponent renderowany
- template lub templateURL (link do pliku) – wzorzec renderowania (HTML)
Wzorce (template) i parametry
We wzorcu możemy używać parametrów - wyrażeń w podwójnych nawiasach klamrowych:
{{wyrażenie}}
- w wyrażeniu możemy używać zmiennych (własności) zadeklarowanych jako public lub @Input()
w komponencie.
@Input
– dekorator pozwalający nadawać zmiennym wartość poprzez własność tagu (znacznika):
@Component({
selector: 'drugi',
template: '<b>{{abc}}</b>'
})
export class DrugiComponent {
@Input()abc;
}
@Component({
selector: 'app-root',
template: `<b>{{wlasnosc}} </b> x {{ zmienna+10 }}
<br /><drugi abc='123'></drugi>
`
})
export class AppComponent {
public wlasnosc='Hello World';
public zmienna=1;
Interfejsy
Jeśli chcemy rozszerzyć funkcjonalność komponentu o typowe funkcje – musimy zaimportować odpowiedni interfejs i użyć go w definicji klasyjako interfejsu.
Część z tych funkcjonalności wymaga dodatkowo zdefiniowania funkcji obsługi zdarzeń (np. z związanych z cyklem życia komponentu). Nazwa zdarzenia = ng+nazwa interfejsu.
Przykład dla interfejsu onInit:
import { Component, OnInit} from '@angular/core';
@Component({
selector: 'testi',
template: 'i={{i}}'
})
export class TestI implements OnInit {
public i=0;
ngOnInit() {
this.i=10;
}
}
Użycie <testi></testi>
spowoduje wyświetlenie i=10
Drzewo komponentów
Komponenty mogą być zagnieżdżane – tworząc drzewo. Między przodkiem i potomkiem drzewa może odbywać się komunikacja. W nawiasach kwadratowych [] określamy własności przekazywane z góry w dół (od rodzica do potomka) – czyli wejście. W nawiasach okrągłych () - zdarzenia(które mogą byćprzekazywane z dołu do góry).Składnia:
- [zmienna]="zmiennaPowiazanaPrzodka"
- (zdarzenie)="funkcja()"
@Component({
selector: 'potomek',
template: `
<button (click)="kliknij()">licznik potomka:{{licznik}} </button><br />
<b>{{zmienna}}</b><br />`
})
export class DrugiComponent {
public licznik = 0;
@Input()zmienna;
kliknij(){
this.licznik=this.licznik+1;
}
}
@Component({
selector: 'app-root',
template: `<potomek [zmienna]=opis></potomek>
`
})
export class AppComponent {
public opis="Opis działania";
}
Typowe zdarzenia
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'potomek',
template: `licznik zmian:{{licznik}} <br />
<b>zmienna (przodka): {{zmienna}}</b><br />`
})
export class DrugiComponent implements OnChanges {
public licznik = 0;
@Input()zmienna;
ngOnChanges(changes: SimpleChanges) {
console.log(changes);
this.licznik=this.licznik+1;
}
}
@Component({
selector: 'app-root',
template: `<potomek [zmienna]=licznikPrzodka></potomek>
<button (click)="kliknij()">zmiana</button>
`
})
export class AppComponent {
public licznikPrzodka=0;
kliknij(){
this.licznikPrzodka=this.licznikPrzodka+1;
}
}
Zdarzenia nietypowe
Do przekazywania nietypowych danych służy klasa EventEmitter który jest rozszerzeniem klasy Subject (z biblioteki RxJS). Może ona rozsyłać zdarzenia niestandardowe przy użyciu swojej metody emit()
Zmienna $event reprezentuje zdarzenie. Może to być natywne zdarzenie przeglądarki (zob. https://developer.mozilla.org/en-US/docs/Web/Events\ lub własne zdarzenie.
Zobacz przykład [006]
Projekcje
Jeśli we wzorcu komponentu test użyjemy:
<ng-content></ng-content>
to użycie tego komonentu spowoduje wstawienie w to miejsce zawartości między <test>
a </test>
.
Na przykład:
<test>przycisk<button>kliknij</button></test>
Można użyć wielung-content z selektorem
Na przykład:
<div><ng-content select="header"></ng-content></div>
<div><ng-content select="section"></ng-content></div>
Użycie:
<test>
<header>nagłówek</header>
<section>treść</section>
</test>
Dyrektywy
Dyrektywy wzbogacają możliwości tworznia szablonów o pętle, warunki, parametryzację klas i styli.
NgStyle
@Component({
selector: 'test',
template: `
<p [ngStyle]="alarmStyle">UWAGA</p>
})
export class TestComponent {
alarmStyle = {'color': 'red' };
}
NgStyle pozwala zmieniać własność style
elementu:
<element [ngStyle]=powiązanie>
Dyrektywę można powiązać z:
a) Strukturą:
[ngStyle]="{color: 'red', 'font-weight': 'bold', borderBottom: borderStyle }"
b) Zmienną:
[ngStyle]="alertStyles"
...
alertStyles = {'color': 'red', 'font-weight': 'bold', 'borderBottom': this.borderStyle };
NgClass
Dyrektywa ngClass zmienia atrybut class , który jest powiązany z komponentem lub elementem, do którego jest przyłączony.
Dyrektywę można wiązać z:
a) łańcuchem znaków (definiuje klasy podane w łańcuchu):
<pngClass="centered-text underlined"
b) tablicą (dodaje klasy z listy)
<p [ngClass]="['warning', 'big']">
c) obiektem (strukturą – stosowane są te nazwy, które w strukturze maja wartość true)
<p [ngClass]="{ card: true, dark: false }">
NgIf
@Component({
selector: 'test',
template: `
<p *ngIf="widoczny">
tekst widoczny gdy zmienna "widoczny" ma wartość true
</p>
`
})
export class TestComponent {
widoczny = true;
NgFor
@Component({
selector: 'test',
template: `
<ul *ngFor="let element of lista">
<li>{{element.name}}</li>
</ul>
`
})
export class TestComponent {
lista = [
{ name: 'Pn', },
{ name: 'Wt', } ];
}
NgSwitch
@Component({
selector: 'app-root',
template: `
<div [ngSwitch]="tab">
<div *ngSwitchCase="1">Zakładka 1</div>
<div *ngSwitchCase="2">Zakładka 2</div>
</div>
`
})
export class AppComponent {
tab = 1;
Zmiana zmiennej tab powoduje usunięcie / dodanie zakładki. Uwaga! Odbywa się to wprost na drzewie DOM - może więc być kosztowne.
Potoki
Składnia: wyrażenie | potok
Przykład:
template: · `<p>Total price={{ price | currency }}</p>`
Potoki z parametrami:
potok: parameter1: parameter2
przykład:
`<p>Total price{{ price | currency: "CAD": true: "1.2-4" }}<p>
Łańcuchy potoków
Przykład:
'<p>Total price of product is {{ price | currency: "CAD": true: "1.2-4" | lowercase }}</p>
Potoki ze stanem
` <p>Total price of product is {{fetchPrice | async | currency:"CAD":true:"1.2-2"}}</p>
Tworzenie własnych potoków
Potok to klasa zdekoratorem @Pipe z metadanymi potoku, wśród których jest własność name . Ta wartość zostanie wykorzystana do wywołania potoku.
Klasa zawierametodę transformacji PipeTransform. Ta metoda pobiera z potoku wartość oraz zmienną liczbę argumentów dowolnego typu i zwracają wartość przekształconą ( piped ).
Obserwacje
Obiekt Observer reaguje na zdarzenia, przekazując je subskrybentom za pomocą obiektu Observable. Podstawową funkcją Observable jest subscribe(), wywoływana z trzema parametrami:
- onNext() - wywoływana po wystąpieniu nowego zdarzenia (jako jedyny jest obowiązkowy).
- onError() - wywoływana po wystąpieniu błędu.
- onCompleted() - wywoływana po zakończeniu sekwencji zdarzeń.
Przepływem zdarzeń można zarządzać za pomocą zestawu operatorów:
import {Component} from '@angular/core';
import {Observable} from 'rxjs/Observable';
@Component({
selector: 'app-root',
template: '...'
})
export class AppComponent {
private strumienDanych: Observable<number>;
private values: Array<number> = [];
private anyErrors: boolean;
private finished: boolean;
constructor() {
}
init() {
this.strumienDanych = new Observable(
observer => {
setTimeout(() => {
observer.next(42);
}, 1000);
setTimeout(() => {
observer.next(43);
}, 2000);
setTimeout(() => {
observer.complete();
}, 3000);
});
let subscription = this.strumienDanych.subscribe(
value => this.values.push(value),
error => this.anyErrors = true,
() => this.finished = true
);
}
}
Importujemy Observable z rxjs, tworzymy strumień danych ( strumienDanych ) typu number. W tym strumieniu (obiekt klasy Observable ) tworzymy subskrypcję ( subscribe ), która definiuje reakcję na dane, błędy i zakończenie strumienia z powodzeniem.
Na strumieniu Observable możemy wykonywać między innymi (pełna lista: http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html, https://xgrommx.github.io/rx-book/content/observable/observable_instance_methods/index.html):
- forEach – pętla for po dostarczanych elementach
- flatMap – łączenie wielu strumieni w jeden
- filter – filtrowanie elementów (jak w operacjach na tablicach)
- map – mapowanie elementów (jak w operacjach na tablicach)
- unsubscribe - wypisanie się ze strumienia
Komunikacja HTTP
import { Http } from '@angular/http';
import { Injectable } from '@angular/core';
// usługa
@Injectable()
export class GetDataService {
constructor(private http: Http) {}
odczyt(url) {
return this.http.get(url)
.map(response => response.json() );
}
}
……
// komponent:
….
constructor(private getData : GetDataService) {
}
odczyt() {
this.getService.odczyt(url)
.subscribe(result => { this.result = result });
}
Formularze
Formularze oparte na szablonach
Najprostszym podejściem do tworzenia formularzy w Angular jest użycie typowych struktur wywodzących się z HTML. Przykład:
import { Component } from'@angular/core';
import {NgForm} from'@angular/forms';
@Component({
selector: 'ngc-form',
templateUrl: './app.forms.component.html'
})
exportclass AppFormsComponent {
formValue = JSON.stringify({});
onSubmit (form: NgForm) {
this.formValue = JSON.stringify(form.value);
}
}
app.forms.component.html
<section>
<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm)">
<labelfor="name">Name</label>
<inputtype="text"name="name"id="name"ngModel>
<buttontype="submit">Submit</button>
</form>
Form Value: {{formValue}}
</section>
Formularze aktywne/oparte na modelu
import { Component } from '@angular/core';
import { FormGroup, FormControl, FormBuilder } from '@angular/forms';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html' })
export class AppComponent {
username = new FormControl('');
password = new FormControl('');
loginForm: FormGroup = this.builder.group(
{
username: this.username,
password: this.password
});
constructor(private builder: FormBuilder) { }
login() {
console.log(this.loginForm.value); // Attempt Logging in... }
}
app/login-form.component.html
<form [formGroup]="loginForm" (ngSubmit)="login()">
<label for="username">username</label><input type="text"
name="username" id="username" [formControl]="username"><br>
<label for="password">password</label>
<input type="password" name="password" id="password" [formControl]="password"><br>
<button type="submit">log in</button>
</form>
Routing
Atrybuty tras:
- path - URL wyświetlany w przeglądarce
- component - wywoływany komponent
- redirectTo - przekierowanie trasy
- pathMatch - opcjonalnie - czy dopasować pełne adresy URL, czy tylko początek. Pusta ścieżka musi mieć pathMatch=full
Przykład:
const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent },
{ path: 'sciezka', component: FunComponent }
];
Wykorzystanie w linkach i menu:
<a routerLink="/sciezka">Kliknij by przejść.....</a>