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łu
  • index.html - strona, na której będzie renderowany komponent
  • main.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>