back to top

Esempio pratico di applicazione Angular

In questa lezione vedremo come creare una semplice applicazione che consente di convertire una certa somma di denaro da una valuta all’altra. Per semplicità la valuta di partenza può essere solo una delle tre principali valute, ovvero Euro, Dollaro e Sterlina inglese.

esempio convertitore valuta angular lista componenti

Nell’immagine mostrata in alto riportiamo il mockup dell’applicazione che vogliamo realizzare. Abbiamo opportunamente evidenziato i diversi componenti che la costituiscono, ovvero un componente base AppComponent in cui sono presenti i due componenti Money e CurrencyList. All’interno del primo abbiamo a sua volta aggiunto un componente CurrencySelector che permette di selezionare la valuta base. Al fine di illustrare in maniera pratica alcuni dei concetti introdotti nelle lezioni precedenti, vogliamo fare in modo che ogni volta che un utente digita una cifra valida o cambia la valuta di base, venga eseguita una richiesta ad un server remoto per prelevare gli ultimi tassi di cambio. Per far ciò ci serviremo di un servizio che implementa anche una forma rudimentale di cache. Infine quando otteniamo i dati dal server, vogliamo mostrare i tassi e la somma convertita in altre valute. Mostriamo quindi una lista di elementi CurrencyListItem.

Partiamo creando un nuovo workspace all’interno di una cartella attraverso il seguente comando messo a disposizione da Angular CLI.

ng new my-angular-app --no-interactive --prefix simple

Ricordiamo che il comando riportato sopra crea un nuovo workspace con un’applicazione di default denominata my-angular-app. Tutti i nomi dei componenti creati avranno ‘simple-‘ come prefisso.

Il componente LoadingSpinnerComponent

Il primo componente che descriviamo non è in realtà presente nell’immagine mostrata sopra. Si tratta di una semplice animazione in puro CSS che verrà mostrata fin quando non sono stati completamente recuperati i dati dal server remoto.

Per creare i file iniziali lanceremo quindi il seguente comando:

ng generate component loading-spinner --no-spec
CREATE src/app/loading-spinner/loading-spinner.component.css (0 bytes)
CREATE src/app/loading-spinner/loading-spinner.component.html (37 bytes)
CREATE src/app/loading-spinner/loading-spinner.component.ts (318 bytes)
UPDATE src/app/app.module.ts (1149 bytes)

Per semplicità non generiamo il file in cui inserire i test di unità, ma creiamo invece i tre file mostrati sopra. Grazie al comando eseguito verrà inoltre modificato il file app.module.ts in cui il componente LoadingSpinnerComponent sarà inserito nell’array dei metadati del decoratore @NgModule.

Lasciamo il file loading-spinner.component.ts intatto mentre modifichiamo i file con estensione .html e .css come mostrato sotto.

<div class="spinner">
  <span></span>
  <span></span>
  <span></span>
  <span></span>
</div>
.spinner {
  overflow: hidden;
  width: 100vw;
}

.spinner span {
  display: block;
  width: 6px;
  height: 6px;
  position: absolute;
  background-color: rgba(255, 255, 255, 0.87);
  border-radius: 50%;
  transform: translateX(-100%);
  animation: move 3s infinite cubic-bezier(.12,.73,.92,.18);
}
.spinner span:nth-child(2) {
  animation-delay: 100ms;
}
.spinner span:nth-child(3) {
  animation-delay: 200ms;
}
.spinner span:nth-child(4) {
  animation-delay: 300ms;
}
@keyframes move {
  0% {transform: translateX(0vw);}
  75% {transform: translateX(100vw);}
  100% {transform: translateX(100vw);}
}

Per il primo componente abbiamo dunque un elemento <div>, che occupa l’intera viewport, all’interno della quale sono presenti 4 elementi di tipo <span> che sono circolari, di colore bianco e hanno un diametro di 6px. Con l’ausilio di una semplice animazione riusciamo ad ottenere l’effetto mostrato nel breve video riportato sopra.

Il componente AppComponent

Il componente AppComponent costituisce il componente base della nostra applicazione. Trascuriamo per il momento il file app.component.css che è possibile comunque ispezionare utilizzando il link al sito stackblitz riportato alla fine della lezione. Concentriamoci invece sui due file app.component.html e app.component.ts.

<!-- app.component.html -->
<div class="wrapper">
  <h1>
    <a href="/" title="Currency Converter Home">
      <img
        src="assets/currency-converter-logo.svg"
        alt=""
        width="36"
        height="36">
      <span>Currency<br>Converter</span>
    </a>
  </h1>
  <main>
    <simple-money
      [baseCurrencies]="baseCurrencies"
      [otherCurrencies]="currencies"
      (updateExchangeRates)="onExchangeRateUpdate($event)"
      (errorMessage)="responseErrorMessage=$event">
    </simple-money>
    <div class="output-container">
      <simple-loading-spinner *ngIf="isLoading"></simple-loading-spinner>
      <simple-currency-list
        *ngIf="!isLoading"
        [rates]="newRates"
        [currencies]="currencies"
        [responseErrorMessage]="responseErrorMessage">
      </simple-currency-list>
    </div>
  </main>
  <footer>
    <p *ngIf="newRates">
      Last Update: {{ newRates.date | date }}
    </p>
  </footer>
</div>

All’interno del template di AppComponent abbiamo un’intestazione di primo livello con il logo del sito. In basso è presente il <footer> con la data dell’ultimo aggiornamento dei tassi di conversioni delle valute. Tali informazioni saranno mostrate solo nel momento in cui sono stati prelevati con successo i dati dal server remoto. All’interno di un elemento <main> sono invece presenti gli elementi relativi ai componenti che andremo presto a creare, ovvero MoneyComponent e CurrencyListComponent.

MoneyComponent presenta due proprietà di input, ovvero baseCurrencies e otherCurrencies che sono due array rappresentanti rispettivamente le sole valute di partenza (Euro, Dollaro, Sterlina) e tutte quelle supportate dall’applicazione. Inoltre MoneyComponent emetterà due eventi o se si è verificato errore (errorMessage) o se è stata inserita una cifra valida e si vuole procedere alla conversione (updateExchangeRates).

CurrencyListComponent riceve invece in ingresso i tassi di cambio, l’array contenente le informazioni su tutte le valute ed un eventuale messaggio di errore nel caso in cui venisse restituito dal server invece dei dati aggiornati. Tale componente viene mostrato solo se è stata completata la richiesta inviata al server remoto. Se quest’ultima è avvenuta con successo, verranno mostrate le informazioni relative alla conversione delle valute, in caso contrario, visualizzeremo un messaggio di errore.

Analizziamo quindi il contenuto del file app.component.ts.

import { Currencies } from './model/currencies.model';
import { Subscription } from 'rxjs';
import { Component, OnInit, OnDestroy } from '@angular/core';

import { ConversionRate } from './model/conversion-rate.model';
import { ConversionRatesService } from './conversion-rates.service';
import { CurrencyListService } from './currency-list.service';

@Component({
  selector: 'simple-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {

  private subscription: Subscription;
  newRates: ConversionRate;
  isLoading = false;
  responseErrorMessage: string;
  baseCurrencies: Currencies;
  currencies: Currencies;

  constructor(
    private conversionRatesService: ConversionRatesService,
    private currencyListService: CurrencyListService
  ) { }

  ngOnInit() {
    this.currencies = this.currencyListService.getCurrencies();
    this.baseCurrencies = this.currencyListService
      .generateBaseCurrencies(['EUR', 'USD', 'GBP']);

    this.subscription = this.conversionRatesService
    .getLoadingStatus()
    .subscribe(isLoading => {
      this.isLoading = isLoading;
    });
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

  onExchangeRateUpdate(newRates: ConversionRate) {
    this.newRates = newRates;
  }
}

AppComponent implementa sia l’interfaccia OnInit che OnDestroy importate entrambi da ‘@angular/core’. Nel costruttore iniettiamo due servizi che andremo a descrivere a breve. Grazie a CurrencyListService otteniamo la lista di valute che useremo nell’applicazione e allo stesso tempo un oggetto, sempre di tipo Currencies, che contiene i dati relativi alle valute di partenza (Euro, Dollaro, Sterlina). Vediamo allora come è definita l’interfaccia Currencies e successivamente analizziamo il contenuto del file currency-list.service.ts.

L’interfaccia Currencies è all’interno del file model/currencies.model.ts che abbiamo generato con Angular CLI (ng generate interface model/Currencies --type=model)

import { Currency } from './currency.model';

export interface Currencies {
  [property: string]: Currency;
}

Gli oggetti che rispettano la struttura dell’interfaccia Currencies prevedono delle chiavi di tipo stringa a cui vengono assegnati degli oggetti di tipo Currency che abbiamo creato sempre con Angular CLI (ng generate interface model/Currency --type=model)

export interface Currency {
  currency: string;
  weight?: number;
  rate?: number;
}

Un oggetto di questo tipo prevede una proprietà currency di tipo stringa che rappresenta il nome esteso della valuta, per esempio ‘euro’, dollar ecc… Può inoltre presentare altre due proprietà opzionali ovvero rate che è di tipo numerico e weight che useremo all’interno del componente CurrencyListComponent per ordinare l’elenco delle diverse valute in base all’importanza che rivestono. Maggiore è il valore di weight e prima verrà mostrata nella lista dei risultati la valuta.

Passiamo ora al contenuto del file currency-list.service.ts.

import { Currencies } from './model/currencies.model';
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class CurrencyListService {

private currencies: Currencies = {
    'EUR': {
      currency: 'euro',
      weight: 10
    },
    'USD': {
      currency: 'dollar',
      weight: 10
    },
    'GBP': {
      currency: 'pound sterling',
      weight: 10
    },
    'AUD': {
      currency: 'australian dollar',
      weight: 9
    },
    'CHF': {
      currency: 'swiss franc',
      weight: 9
    },
    'JPY': {
      currency: 'japanese yen',
      weight: 8
    },
    'CNY': {
      currency: 'chinese yuan',
      weight: 8
    },
    'CAD': {
      currency: 'canadian dollar',
      weight: 8
    },
    'NZD': {
      currency: 'new zealand dollar',
      weight: 6
    },
    'NOK': {
      currency: 'norwegian krone',
      weight: 6
    },
    'INR': {
      currency: 'indian rupee',
      weight: 6
    }
  };

  getCurrencies(): Currencies {
    return this.currencies;
  }

  generateBaseCurrencies(currencyCodes: string[]): Currencies {
    const baseCurrencies = currencyCodes.reduce((prevValue, currentValue) => {
      if (this.currencies[currentValue]) {
        prevValue[currentValue] = this.currencies[currentValue];
      }
      return prevValue;
    }, {});
    return baseCurrencies;
  }
}

Si tratta di un servizio registrato con il Root Injector attraverso un provider specificato nei metadati del decoratore @Injectable. È quindi presente una sola istanza che è accessibile in tutta l’applicazione. CurrencyListService presenta un array predefinito di valute il quale viene restituito dal metodo getCurrencies() che abbiamo invocato nel componente AppComponent. Sempre in quest’ultimo avevamo ottenuto un oggetto di valute di partenze grazie al metodo generateBaseCurrencies(currencyCodes: string[]) che accetta come unico argomento un array di codici ISO identificativi di ciascuna valuta e restituisce un nuovo oggetto di tipo Currencies sfruttando semplicemente il metodo Array.prototype.reduce() (Per maggiori informaizoni sul metodo reduce potete consultare MDN web docs).

Tornando invece al componente AppComponent, nel metodo OnInit oltre ad ottenere la lista iniziale delle valute, invochiamo anche il metodo subscribe() dell’Observable restituito dal metodo this.conversionRatesService.getLoadingStatus() che emette un nuovo valore per notificare se il servizio conversionRatesService ha eseguito una nuova richiesta (isLoading=true) e quando quest’ultima è stata completata (isLoading=false). In questo modo possiamo settare la proprietà booleana isLoading del componente AppComponent che permette di decidere se mostrare la lista dei risultati o l’animazione che indica all’utente che una richiesta è ancora in corso. Infine il metodo onExchangeRateUpdate() viene invocato ogni volta che il componente MoneyComponent emette un evento includendo i nuovi tassi di conversione recuperati. Così facendo, usiamo il componente AppComponent per mettere in comunicazione i due componenti adiacenti MoneyComponent e CurrencyListComponent e trasferire delle informazioni dal primo al secondo.

Il componente MoneyComponent e CurrencySelectorComponent

Iniziamo allora ad analizzare i file money/money.component.html e money/money.component.ts relativi a MoneyComponent, creato sempre con l’ausilio di Angular CLI.

ng generate component money --no-spec
CREATE src/app/money/money.component.css (0 bytes)
CREATE src/app/money/money.component.html (25 bytes)
CREATE src/app/money/money.component.ts (272 bytes)
UPDATE src/app/app.module.ts (1105 bytes)

Nel template di MoneyComponent abbiamo un campo di input che riceve immediatamente il focus quando la pagina ha terminato il caricamento. Utilizziamo l’attributo pattern per indicare il formato accettato dal campo e quindi fornire all’utente un messaggio di feedback nel caso vengano inseriti dei caratteri non validi. Ci affidiamo inoltre alla direttiva ngModel sia per la validazione del campo che per invocare il metodo onInput() ogni volta che viene digitato un nuovo carattere. Abbiamo infine definito una variabile di template #money per accedere più semplicemente alle proprietà che consentono di capire se il valore inserito è valido o meno.

<div class="money-container">
  <div class="money">
    <label for="money" aria-hidden="true">
      Amount
    </label>
    <input
      type="text"
      id="money"
      name="money"
      [attr.aria-label]="
        'Amount in ' + 
        baseCurrencies[currentCurrencyCode].currency + 
        'to be converted'"
      ngModel
      (ngModelChange)="onInput($event)"
      pattern="^d{1,3}(,?d{3})*(.d{2})?$"
      inputmode="numeric"
      #money="ngModel"
      autofocus
      autocomplete="off"
    >
  </div>
  <div class="error-message" role="alert">
    <span *ngIf="money.errors && (money.dirty || money.touched)">
      Please enter just a valid number
    </span>
  </div>
  <simple-currency-selector
    [currencyCodes]="baseCurrencyCodes"
    (selectedCurrency)="onCurrencySelection($event)"
  >
  </simple-currency-selector>
</div>

Sempre all’interno del template abbiamo l’elemento <simple-currency-selector> relativo a CurrencySelectorComponent il quale riceve in ingresso un array con i codici ISO di tre lettere che identificano ciascuna valuta ed emette un evento ogni volta che viene selezionata una diversa valuta. In tal caso invochiamo il metodo onCurrencySelection() che descriveremo a breve. Prima però analizziamo i file currency-selector.component.ts e currency-selector.component.html.

<div class="selector">
  <label *ngFor="let currencyCode of currencyCodes">
    <input
      type="radio"
      name="currency-selector"
      id="selector-{{currencyCode}}"
      [value]="currencyCode"
      [checked]="currentCurrencyCode == currencyCode"
      (change)="onChange($event)"><span>{{ currencyCode }}</span>
  </label>
</div>

Nel file currency-selector.component.html abbiamo tre elementi di input di tipo radio. (uno per ogni codice iso identificativo della valuta che viene passato in ingresso al componente) Ogni volta che l’utente seleziona una diversa valuta, viene invocato il metodo onChange(). Il valore di ciascun campo è settato tramite binding delle proprietà. Mentre la proprietà checked sarà pari a true quando il valore del campo di input coincide con quello della proprietà currentCurrencyCode mantenuta nel componente.

import { 
  Component, 
  Input, 
  Output, 
  EventEmitter, 
  OnInit 
} from '@angular/core';

@Component({
  selector: 'simple-currency-selector',
  templateUrl: './currency-selector.component.html',
  styleUrls: ['./currency-selector.component.css']
})
export class CurrencySelectorComponent implements OnInit {

  @Input()
  currencyCodes: string[];

  @Output()
  selectedCurrency: EventEmitter<string> = new EventEmitter<string>();

  currentCurrencyCode: string;

  ngOnInit() {
    this.currentCurrencyCode = this.currencyCodes[0];
  }

  onChange(event: Event) {
    this.currentCurrencyCode = (<HTMLInputElement>event.target).value;
    this.selectedCurrency.emit(this.currentCurrencyCode);
  }

}

Nel file currency-selector.component.ts inizializziamo la proprietà currentCurrencyCode che mantiene il valore del codice identificativo della valuta selezionata dall’utente. (Per esempio ‘EUR’o ‘USD’) Quando l’utente seleziona una nuova valuta, viene aggiornato il suo valore grazie al metodo onChange() e viene successivamente emesso un evento contenente il valore del codice ISO dellla valuta selezionata. Tale valore sarà quindi usato dal componente MoneyComponent che riportiamo sotto.

import { Currencies } from './../model/currencies.model';
import { 
  Component, 
  OnInit, 
  ViewChild,
  Input,
  Output, 
  EventEmitter, 
  OnDestroy 
} from '@angular/core';

import { 
  Subject, 
  BehaviorSubject, 
  combineLatest, 
  Subscription 
} from 'rxjs';

import { 
  filter, 
  switchMap, 
  debounceTime, 
  distinctUntilChanged 
} from 'rxjs/operators';
import { NgModel } from '@angular/forms';

import { ConversionRatesService } from './../conversion-rates.service';
import { ConversionRate } from './../model/conversion-rate.model';

@Component({
  selector: 'simple-money',
  templateUrl: './money.component.html',
  styleUrls: ['./money.component.css']
})
export class MoneyComponent implements OnInit, OnDestroy {

  @Input() baseCurrencies: Currencies;

  @Input() otherCurrencies: Currencies;

  @Output()
  updateExchangeRates: EventEmitter<ConversionRate> =
    new EventEmitter<ConversionRate>();

  @Output()
  errorMessage: EventEmitter<string> = new EventEmitter<string>();

  baseCurrencyCodes: string[];
  currentCurrencyCode: string;
  currentCurrencyCodeSubject: BehaviorSubject<string>;
  otherCurrencyCodes: string[];

  @ViewChild(NgModel) moneyInputModel: NgModel;

  currencyPattern = new RegExp(`[,]`, 'g');

  valueSubject = new Subject<number>();

  observableSubscription: Subscription;

  constructor(private conversionRatesService: ConversionRatesService) { }

  ngOnInit() {
    this.baseCurrencyCodes = Object.keys(this.baseCurrencies);
    this.currentCurrencyCode = this.baseCurrencyCodes[0];
    
    // emette un valore tramite il suo metodo next()
    // quando viene selezionata una nuova valuta
    this.currentCurrencyCodeSubject = 
      new BehaviorSubject<string>(this.currentCurrencyCode);
    
    this.otherCurrencyCodes = Object.keys(this.otherCurrencies);

    // emette i valori del campo di input
    // quando viene invocato il metodo onInput()
    // definito in basso 
    const valuesObs = this.valueSubject.pipe(
      filter(value => value > 0 && !isNaN(value) && isFinite(value)),
      debounceTime(800),
      distinctUntilChanged()
    );

    const combinedDataObs = combineLatest(
      valuesObs,
      this.currentCurrencyCodeSubject
    );

    this.observableSubscription = combinedDataObs
      .pipe(
        distinctUntilChanged((arr1, arr2) => {
          // in posizione 0 è presente il valore del campo di input
          // in posizione 1 il codice ISO della valuta
          // effettuiamo una richiesta solo se almeno uno dei due è cambiato
          return arr1[0] === arr2[0] && arr1[1] === arr2[1];
        }),
        switchMap(
          (dataArray) => 
            this.conversionRatesService.getLatestExchangeRates(
              dataArray[0], // importo
              dataArray[1], // codice della valuta di base
              this.otherCurrencyCodes
                .filter(item => item !== this.currentCurrencyCode)
            )
        )
      ).subscribe(
        (value) => {
          // notifica il componente AppComponent
          // dell'arrivo dei nuovi tassi di cambio
          this.updateExchangeRates.emit(value);
          this.conversionRatesService.stopLoading();
        },
        (error) => {
          this.conversionRatesService.stopLoading();
          this.errorMessage.emit(error.message);
        }
      );
  }

  onCurrencySelection(newCurrencyCode: string) {
    this.currentCurrencyCode = newCurrencyCode;
    this.currentCurrencyCodeSubject.next(this.currentCurrencyCode);
  }

  onInput(value: string) {
    const newValue = value.replace(this.currencyPattern, '');
    if (this.moneyInputModel.valid) {
      this.valueSubject.next(Number(newValue));
    }
  }

  ngOnDestroy() {
    this.observableSubscription.unsubscribe();
  }
}

MoneyComponent è probabilmente il componente di maggior rilievo all’interno dell’applicazione. Abbiamo visto che riceve in ingresso due oggetti di tipo Currencies ed emette un evento per avvertire il componente AppComponent della ricezione dei nuovi tassi di cambio o del verificarsi di un errore. Partendo dal costruttore, notiamo che abbiamo iniettato un’istanza del servizio ConversionRatesService. Nel metodo ngOnInit inizializziamo poi la proprietà currentCurrencyCode col valore del codice ISO della valuta predefinita. Abbiamo successivamente creato currentCurrencyCodeSubject che è una proprietà di tipo BehaviorSubject che abbiamo inizializzato col valore della proprietà currentCurrencyCode. Sempre in ngOnInit ci assicuriamo che i valori emessi da valueSubject vengano filtrati facendo passare solo valori numerici finiti. Sfruttiamo inoltre gli operatori debounceTime e distinctUntilChanged per fare in modo che vengano emessi i valori solo quando l’utente ha smesso di digitare dei caratteri nel campo di input, attraverso la tecnica descritta nella lezione su RxJS in cui abbiamo analizzato il funzionamento dell’operatore debounceTime(). Evidenziamo che valueSubject emette un valore ogni volta che viene invocato il suo metodo next() all’interno del metodo onInput() del componente. Invece currentCurrencyCodeSubject emette un nuovo valore all’interno del metodo onCurrencySelection() che viene chiamato quando l’utente seleziona una nuova valuta nel componente CurrencySelectorComponent. Tornando di nuovo al metodo ngOnInit utilizziamo l’operatore combineLatest per combinare i valori emessi dai due Observable associati al campo di input e al selettore di valuta. L’operatore combineLatest restituisce un nuovo Observable che emette un array, contenente i valori dei due Observable di ingresso, quando uno dei due invia a sua volta un nuovo valore. L’array contiene dunque il nuovo valore emesso da uno dei due Observable di ingresso e l’ultimo valore emesso precedentemente dall’altro Observable. (Per capire meglio come funziona l’operatore combineLatest, potete provare l’esempio che trovate su stackblitz) Per l’Observable combinedDataObs ci assicuriamo che venga emesso un nuovo dato solo se il valore del campo di input o la valuta selezionata vengono effettivamente cambiati. In tal caso utilizziamo i valori dell’array per invocare il metodo conversionRatesService.getLatestExchangeRates() a cui passiamo come primo argomento l’importo da convertire, come secondo argomento il codice ISO della valuta e come terzo argomento l’array dei codici ISO delle valute delle quali si vuole ottenere il tasso di cambio rispetto alla valuta base. Notate che applichiamo l’operatore switchMap perché il metodo conversionRatesService.getLatestExchangeRates() restituisce a sua volta un Observable dal quale vogliamo estrarre il valore emesso, ovvero l’oggetto contenente i tassi di conversione. (In una delle precedenti lezioni in cui abbiamo parlato di RxJS, abbiamo descritto in maniera più approfondita il funzionamento dell’operatore switchMap e in generale dei cosiddetti Higher Order Observables). Infine quando riceviamo i nuovi tassi di cambio dal servizio, emettiamo un evento per notificare il componente AppComponent e impostiamo la proprietà privata isLoading del servizio al valore false attraverso il metodo conversionRatesService.stopLoading().

A questo punto possiamo analizzare il codice del servizio ConversionRatesService che consente di ottenere i tassi di conversione effettuando una richiesta ad un server remoto. A scopo didattico utilizzeremo l’API gratuita fornita dal sito openrates.io visto che non richiede nessuna iscrizione o chiave di autenticazione. I tassi di conversione forniti non sono aggiornati in tempo reale, ma per questo esempio costituiscono comunque una solida opzione.

import {Injectable} from '@angular/core';
import {
  HttpClient, 
  HttpResponse, 
  HttpErrorResponse
} from '@angular/common/http';
import { Observable, Subject, of, throwError } from 'rxjs';
import { map, tap, catchError } from 'rxjs/operators';

import { ConversionRate } from './model/conversion-rate.model';
import { ExchangeRateItem } from './model/exchange-rate-item.model';

@Injectable({providedIn: 'root'})
export class ConversionRatesService {

  private baseUrl = 'https://api.exchangeratesapi.io/latest';
  private _loading: boolean;
  private loadingStatus: Subject<boolean> = new Subject<boolean>();

  private cache = {};

  constructor(private http: HttpClient) {}

  getLatestExchangeRates(
    amount: number,
    baseCurrencyCode: string,
    otherCurrencyCodes: string[]
  ): Observable <ConversionRate> {
    let url = `${this.baseUrl}?base=${baseCurrencyCode}`;
    url += `&symbols=${otherCurrencyCodes.join(',')}`;

    let conversionRateObj: ConversionRate;

    this.startLoading();

    if (this.cache && this.cache[baseCurrencyCode]) {

      if (amount !== this.cache[baseCurrencyCode].amount) {
        conversionRateObj = new ConversionRate(
          amount,
          baseCurrencyCode,
          this.cache[baseCurrencyCode].date,
          this.cache[baseCurrencyCode].rates
        );
        return of(conversionRateObj);
      }

      return of(this.cache[baseCurrencyCode]);

    } else {

      return this.http.get(url, {observe: 'response'}).pipe(
        map((response: HttpResponse<ExchangeRateItem>) => {
          if (response.ok && response.status === 200) {
            return new ConversionRate(
              amount,
              baseCurrencyCode,
              new Date(response.body.date),
              response.body.rates
            );
          }
        }),
        tap(
          lastConversionRate => 
            this.cache[baseCurrencyCode] = lastConversionRate
        ),
        catchError(
          (
            error: HttpErrorResponse, 
            originalObs: Observable<ConversionRate>
          ) => {
          return throwError(
            new Error(
              'oops! We couldn't get the latest rates. Please try later.'
            ));
        })
      );
    }
  }

  get loading(): boolean {
    return this._loading;
  }

  set loading(value) {
    this._loading = value;
    this.loadingStatus.next(value);
  }

  startLoading() {
    this.loading = true;
  }

  stopLoading() {
    this.loading = false;
  }

  getLoadingStatus() {
    return this.loadingStatus.asObservable();
  }
}

Nel frammento di codice riportato sopra definiamo l’url base del servizio oltre ad una variabile privata _loading che consentirà di capire quando una richiesta è stata completata. Grazie all’oggetto loadingStatus di tipo Subject comunicheremo ai componenti quando il valore della variabile privata _loading cambia. Nel costruttore iniettiamo invece un altro servizio, ovvero HttpClient che useremo per effettuare una richiesta di tipo GET al server remoto.

Il metodo più importante è però getLatestExchangeRates() che abbiamo usato in MoneyComponent per ottenere di volta in volta i tassi di conversione delle valute. Se non è già stato salvato in cache un risultato, effettuiamo una richiesta al server remoto chiedendo al metodo http.get() di fornirci l’intero oggetto di risposta ({observe: ‘response’}) in modo tale da poter valutare se la nostra richiesta è andata a buon fine (response.status === 200) e restituire in questo caso un oggetto di tipo ConversionRate. Quest’ultima è una classe definita all’interno della cartella model presente nella directory base del nostro progetto come mostrato sotto.

import { ExchangeRates } from './exchange-rates.model';

export class ConversionRate {
  constructor(
    public amount: number,
    public baseCurrencyCode: string,
    public date: Date,
    public rates: ExchangeRates
  ) { }
}
export interface ExchangeRates {
  [property: string]: number;
}

Continuando ad analizzare il metodo getLatestExchangeRates(), grazie all’opzione specificata, il metodo http.get() fornisce una risposta HTTP completa. Si tratta di un oggetto di tipo HTTPResponse che è una classe generica. Abbiamo quindi indicato che il suo campo ‘body’ è di tipo ExchangeRateItem. Quest’ultima è un’interfaccia da noi definita in base alla struttura dell’oggetto restituito dal server remoto.

import { ExchangeRates } from './exchange-rates.model';

export interface ExchangeRateItem {
  base: string;
  date: string;
  rates: ExchangeRates;
}

Oltre a restituire l’oggetto creato, conserviamo una copia in cache in modo da poterla usare per le successive richieste. Se viene trovato l’oggetto in cache per una certa valuta, viene restituito subito un Observable che emette un oggetto di tipo ConversionRate. Se necessario viene aggiornato l’importo di tale oggetto prima di essere restituito.

In caso di errore restituiamo invece un nuovo Observable tramite l’operatore throwError il cui messaggio di errore viene poi mostrato nella nostra applicazione.

Infine, una volta ottenute le necessarie informazioni attraverso il servizio ConversionRatesService, le mostriamo all’interno dell’applicazione grazie ai due componenti CurrencyListComponent e CurrencyListItemComponent.

import { Currencies } from './../model/currencies.model';
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';

import { ConversionRate } from './../model/conversion-rate.model';

@Component({
  selector: 'simple-currency-list',
  templateUrl: './currency-list.component.html',
  styleUrls: ['./currency-list.component.css']
})
export class CurrencyListComponent implements OnChanges {

  @Input() rates: ConversionRate;
  @Input() responseErrorMessage: string;
  @Input() currencies: Currencies;
  wCurrencies: Currencies;

  ngOnChanges(changes: SimpleChanges) {
    const rates = changes.rates;
    const keys = Object.keys(this.currencies);
    if (
      rates && rates.currentValue && 
      (!rates.previousValue ||
        (
          rates.previousValue && 
          rates.previousValue.baseCurrencyCode !== rates.currentValue.baseCurrencyCode
        )
      )
    ) {
      this.wCurrencies = keys
        .filter(key => key !== this.rates.baseCurrencyCode)
        .reduce((prevValue, currentValue) => {
          prevValue[currentValue] = {
            ...this.currencies[currentValue],
            rate: this.rates.rates[currentValue]
          };
          return prevValue;
        }, {});
    }
  }

  customListOrder(obj1, obj2): number {
    return obj2.value.weight - obj1.value.weight;
  }
}

CurrencyListComponent implementa OnChanges e nel metodo ngOnChanges facciamo in modo che se l’utente ha selezionato una valuta diversa dalla richiesta precedente, costruiamo l’oggetto wCurrencies che è di tipo Currencies a partire dall’oggetto currencies. In particolare prendiamo quest’ultimo, rimuoviamo le informazioni sulla valuta di base e arricchiamo l’oggetto relativo alle altre valute con il tasso di cambio rispetto alla valuta di base che abbiamo ottenuto tramite il servizio ConversionRatesService. Usiamo invece il metodo customListOrder() per ordinare i risultati in base al valore della proprietà weight presente nell’oggetto relativo a ciascuna valuta.

<div>
  <div role="alert">
    <p 
      class="error-message" 
      *ngIf="!rates && responseErrorMessage">
        {{ responseErrorMessage }}
    </p>
  </div>
  <p 
    *ngIf="!rates 
    && !responseErrorMessage">
      Insert the amount that you want to convert in the field above
  </p>
  <ul *ngIf="rates">
    <simple-currency-list-item
      *ngFor="let currencyDetails of wCurrencies | keyvalue:customListOrder"
      [amount]="rates.amount"
      [otherCurrencyConversionRate]="currencyDetails.value.rate"
      [baseCurrencyCode]="rates.baseCurrencyCode"
      [otherCurrencyCode]="currencyDetails.key"
    >
    </simple-currency-list-item>
  </ul>
</div>

All’interno del template usiamo l’oggetto wCurrencies e attraverso la pipe keyvalue otteniamo un array in cui ogni oggetto ha due proprietà key e value che sono rispettivamente le chiavi e i valori presenti nell’oggetto di partenza. In questo modo possiamo utilizzare la direttiva *ngFor e generare ciascun elemento <simple-currency-list-item> in maniera più semplice e rapida.

import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { CurrencyPipe } from '@angular/common';

@Component({
  selector: 'simple-currency-list-item',
  templateUrl: './currency-list-item.component.html',
  styleUrls: ['./currency-list-item.component.css']
})
export class CurrencyListItemComponent {

  @Input() baseCurrencyCode: string;
  @Input() amount: number;
  @Input() otherCurrencyCode: string;
  @Input() otherCurrencyConversionRate: number;

}
<li>
  <div
    class="convertion-rates-container"
    [attr.aria-label]="
      'Convertion result from ' + 
      baseCurrencyCode + ' to ' + otherCurrencyCode">
    <span
      class="conversion-rate"
      [attr.aria-label]="
        '1 ' + baseCurrencyCode + ' is equal to ' + 
        (otherCurrencyConversionRate | number:'1.2-4') + otherCurrencyCode">
      1 {{ baseCurrencyCode  }} ➔ 
      {{ otherCurrencyConversionRate | number:'1.2-4' }} 
      {{ otherCurrencyCode }}
    </span>
    <span
      class="conversion-rate"
      [attr.aria-label]="
        '1 ' + otherCurrencyCode + ' is equal to ' + 
        (1 / otherCurrencyConversionRate | number:'1.2-4') + 
        ' ' + otherCurrencyCode">
        1 {{ otherCurrencyCode  }} ➔ 
        {{ 1 / otherCurrencyConversionRate | number:'1.2-4' }} 
        {{ baseCurrencyCode }}
    </span>
  </div>
  <div
    class="converted-money"
    [attr.aria-label]="amount + ' ' + baseCurrencyCode + 
      ' is equal to ' + 
      (amount * otherCurrencyConversionRate | number:'1.2-2') + ' ' +  
      otherCurrencyCode">
    {{ amount * otherCurrencyConversionRate | currency:otherCurrencyCode }}
  </div>
</li>

Il componente CurrencyListItemComponent non fa altro che mostrare le informazioni relative alla conversione dalla valuta base a ciascuna delle altre valute supportate all’interno dell’applicazione.

Conclusioni

In questa lezione abbiamo mostrato un esempio di applicazione Angular in cui abbiamo unito in un unico esempio gran parte dei concetti illustrati nelle precedenti lezioni. Nella prossima lezione invece discuteremo dei moduli, per poi concludere questa guida con un esempio attraverso il quale descriveremo il funzionamento di Angular Router che abilita la navigazione da una vista ad un’altra in risposta a determinate azioni degli utenti all’interno di una Single-Page Application (SPA).

Per analizzare meglio il codice dell’esempio descritto in questa lezione, potete visualizzarlo nell’editor che trovate su stackblitz o nel riquadro sottostante.

Alla prossima lezione!

Pubblicitร