08 - Angular

08 - Angular

Intro

Angular è un framework che mette a disposizione una serie di funzionalità che semplificano lo sviluppo di applicazioni sofisticate, come ad esempio:

  • components per dividere la parte grafica in codice riutilizzabile
  • services per separare le logiche di elaborazione dai componenti
  • routing per gestire la navigazione tra le pagine
  • HttpClientModule per eseguire richieste di dati tramite api
  • directives per aggiungere funzionalità a tag e componenti esistenti
  • modules per dividere la logica in base alle funzionalità e renderla potenzialmente riutilizzabile
  • dependency injection per fornire servizi e configurazioni alle varie parti dell'applicazione in modo configurabile e sostituibile
  • suite di testing
  • ecc

installare angular

npm install -g @angular/cli

npm è il package manager di nodejs, permette di installare librerie in un progetto o globalmente. in questo caso il pacchetto @angular/cli aggiunge dei comandi alla shell, per questo motivo lo installiamo globalmente nel sistema usando -g

creare l'applicazione

ng new its-cart
cd its-cart
code .

in VSCode installare l'estensione Angular Language Service

Avviare l'app

ng serve
# oppure
ng serve --open
# per aprire direttamente il browser

Struttura di un'applicazione

Tutto il codice che viene sviluppato risiede dentro la cartella src, tutto quello che è fuori è configurazione o file di appoggio necessari alla compilazione

angular.json

Contiene le configurazioni che usa la cli quando usiamo i comandi serve, build e altri.
Contiene la lista dei progetti, ogni progetto ha diverse proprietà:

  • cartella codice sorgente
  • tipo di progetto
  • cartella output
  • configurazione dei task (oggetto architect)

tsconfig.json

contiene la configurazione di typescript. Gli altri due file tsconfig.app.json e tsconfig.spec.json estendono il comportamento del file base. quale file viene utilizzato a seconda del task è specificato nel file angular.json.

package.json

e' il file che contiene le definizioni di un progetto di node, contiene

  • nome
  • descrizione
  • comandi (script)
  • dipendenze
  • requisiti
  • autore, ecc

Installare Angular Bootstrap

Alcuni pacchetti scritti apposta per angular hanno degli script per l'installazione che vanno a modificare file i file di configurazione e i file del progetto per funzionare con la libreria installata.

ng add @ng-bootstrap/ng-bootstrap

Andando a vedere il codice sono state aggiunte:

  • le dipendenze nel file package.json
  • aggiunti gli stili al file angular.json
  • aggiunti dei tipi ai file tsconfig
  • aggiunti l'import del modulo nel file app.module.ts

Esercizio

  • spostare il template della lista dentro app component con *ngFor
  • spostare il file cart-utils nel progetto mettendo i tipi dove richiesto
  • creare i metodi getItemPrice e getDiscountAmount in AppComponent
  • aggiungere [(ngModel)] alla quantità per mostrare che vengono ricalcolati i valori a ogni modifica
    • mettere console.log dentro i metodi per vedere quando vengono ricalcolati

app.component.ts

import { Component } from '@angular/core';
import { getDiscountAmount, getDiscountedPrice, getFinalPrice, getVat } from './cart-utils';

const CART = [
  {
    id: 1,
    name: 'ssd',
    netPrice: 95,
    weight: 100,
    discount: 5,
    quantity: 2
  },
  {
    id: 2,
    name: 'motherboard',
    netPrice: 270,
    weight: 900,
    discount: 0,
    quantity: 1
  },
  {
    id: 3,
    name: 'ram',
    netPrice: 120,
    weight: 60,
    discount: 10,
    quantity: 2
  },
  {
    id: 4,
    name: 'processor',
    netPrice: 400,
    weight: 130,
    discount: 0,
    quantity: 1
  },
  {
    id: 5,
    name: 'power supply',
    netPrice: 130,
    weight: 1400,
    discount: 15,
    quantity: 1
  },
  {
    id: 6,
    name: 'cpu cooler',
    netPrice: 170,
    weight: 1000,
    discount: 23,
    quantity:1
  },
  {
    id: 7,
    name: 'gpu',
    netPrice: 1600,
    weight: 2500,
    discount: 0,
    quantity: 1
  },
  {
    id: 8,
    name: 'case',
    netPrice: 130,
    weight: 3500,
    discount: 30,
    quantity: 1
  }
];

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  items = CART;
  vat = getVat('IT');

  getItemPrice(item: any) {
    const discountedPrice = getDiscountedPrice(item.netPrice, item.discount);
    return getFinalPrice(discountedPrice * item.quantity, this.vat);
  }

  getDiscountAmount(item: any) {
    return getDiscountAmount(item.netPrice, item.discount) * item.quantity;
  }
}

app.component.html

<div class="container">
  <div id="items-list-container">
    <h3>I tuoi articoli</h3>
    <ul class="list-group" id="items-list">
      <li class="list-group-item item" *ngFor="let item of items">
        <div class="d-flex flex-row d-flex justify-content-between align-items-center">
          <div class="item-name">
              {{item.name}}
          </div>

          <div class="d-flex flex-row align-items-center justify-content-end flex-wrap">
              <span class="ms-2 d-flex flex-row align-items-center">
                  <label class="me-1" for="quantity">qty:</label>
                  <input class="form-control item-quantity" [(ngModel)]="item.quantity" type="number" name="quantity" style="width: 70px">
              </span>
              <span class="ms-2">
                  <span class="item-price">{{getItemPrice(item)}}</span>
                  <span class="item-discount">(-{{getDiscountAmount(item)}})</span>
              </span>
          </div>
        </div>
      </li>
    </ul>
  </div>

  <div class="mt-5">
    <h3>Totale ordine</h3>
    <div class="order-summary">

    </div>
  </div>
</div>

Suddivisione in component e services

Generalmente si cerca di dividere i componenti di una applicazione tra Smart e Dumb component (o Smart e Presentational).

Dumb components

Sono quei componenti che da soli non hanno un vero scopo se non quello di rappresentare dei dati nel DOM. Questi componenti ricevono dei dati in input, li modificano, li presentano ed emettono eventi per notificare un'azione dell'utente.
Un dumb component non ha nessuna conoscenza dello stato dell'applicazione né contiene alcuna business logic.

Smart components

Sono quelli che sanno come trattare e manipolare i dati, quelli che conoscono e sono legati alla logica dell'applicazione. Questi componenti ritornano i dati (generalmente dai service), sanno come elaborarli, modificarli, salvarli e li passano in input ad altri componenti.

Nel nostro esempio il sommario e il singolo elemento della lista sono Dumb components, mentre ci sarà un componente della pagina che sa come andare a prendere gli elementi del carrello, cosa fare quando cambia la quantità e a che componenti passare questi dati per visualizzarli nella pagina.

Info

Non necessariamente la logica di uno Smart component risiede totalmente in esso. Spesso il componente richiama metodi e riceve dati da dei service, questo non significa che non sia smart perché è comunque lui che decide cosa fare con i dati. Cosa che un Dumb component non fa

Creiamo il componente CartItem:

  • spostiamo il template del singolo componente (senza li per il momento perché dobbiamo definire solo il contenuto, poi andremo a modificare il funzionamento)
ng generate component components/cart-item

cart-item.component.html

<div class="d-flex flex-row d-flex justify-content-between align-items-center">
  <div class="item-name">
      {{item.name}}
  </div>

  <div class="d-flex flex-row align-items-center justify-content-end flex-wrap">
      <span class="ms-2 d-flex flex-row align-items-center">
          <label class="me-1" for="quantity">qty:</label>
          <input class="form-control item-quantity" [(ngModel)]="item.quantity" type="number" name="quantity" style="width: 70px">
      </span>
      <span class="ms-2">
          <span class="item-price">{{getItemPrice()}}</span>
          <span class="item-discount">(-{{getDiscountAmount()}})</span>
      </span>
  </div>
</div>

cart-item.component.ts

import { Component, Input } from '@angular/core';
import { getDiscountAmount, getDiscountedPrice, getFinalPrice } from 'src/app/cart-utils';

@Component({
  selector: 'app-cart-item',
  templateUrl: './cart-item.component.html',
  styleUrls: ['./cart-item.component.css']
})
export class CartItemComponent {
  @Input()
  item: any;

  @Input()
  vat: number = 0;

  getItemPrice() {
    const discountedPrice = getDiscountedPrice(this.item.netPrice, this.item.discount);
    return getFinalPrice(discountedPrice * this.item.quantity, this.vat);
  }

  getDiscountAmount() {
    return getDiscountAmount(this.item.netPrice, this.item.discount) * this.item.quantity;
  }
}

C'è un problema con questa implementazione: ho delegato al componente il compito di calcolare i prezzi ogni volta che cambia la quantità. Questa logica non dovrebbe risiedere in un dumb component, il suo compito è di mostrarmi i prezzi degli articoli che gli mando e notificare a AppComponent che la quantità è cambiata.
Per fare questo devo separare le due funzionalità di ngModel, in modo da andare a settare il valore quando me ne arriva uno nuovo, e lanciare un evento quando viene modificata la quantità.

cart-item.component.html

<input class="form-control item-quantity" [ngModel]="item.quantity" (ngModelChange)="quantityChange($event)" ...

cart-item.component.ts

import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { getDiscountAmount, getDiscountedPrice, getFinalPrice } from 'src/app/cart-utils';

@Component({
 selector: 'app-cart-item',
 templateUrl: './cart-item.component.html',
 styleUrls: ['./cart-item.component.css'],
 changeDetection: ChangeDetectionStrategy.OnPush
})
export class CartItemComponent {
 @Input()
 item: any;

 @Input()
 vat: number = 0;

 @Output('quantityChange')
 onQuantityChange = new EventEmitter<number>();

...

 quantityChange(event: number) {
   this.onQuantityChange.emit(event);
 }
}

app.component.html

<app-cart-item [item]="item" [vat]="vat" (quantityChange)="changeQuantity(item, $event)"></app-cart-item>

app.component.ts

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  items = CART;
  vat = getVat('IT');


  changeQuantity(item: any, newQuantity: number) {
    item.quantity = newQuantity;
  }
}

Summary component

ng generate component components/summary

summary.component.html

<div>
  <div class="d-flex justify-content-between">
    <span>Net Total:</span>
    <span>{{summary.netTotal}}</span>
  </div>

  <div class="d-flex justify-content-between">
    <span>VAT:</span>
    <span>{{summary.totalVat}}</span>
  </div>
  <div class="d-flex justify-content-between">
    <span>Transport:</span>
    <span >{{summary.transportFee}}</span>
  </div>
  <hr>
  <div class="d-flex justify-content-between">
    <span>Total:</span>
    <span>{{summary.totalPrice}}</span>
  </div>
</div>

summary.component.ts

import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
import { getDiscountedPrice, getFinalPrice, getTransportFee, getVatAmount } from 'src/app/cart-utils';

@Component({
  selector: 'app-summary',
  templateUrl: './summary.component.html',
  styleUrls: ['./summary.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SummaryComponent {
  @Input()
  set items(value: any[]) {
    this._items = value;
    this.summary = this.updateSummary();
  }

  protected _items: any[] = [];

  @Input()
  set vat(value: number) {
    this._vat = value;
    this.summary = this.updateSummary();
  }

  protected _vat = 0;

  summary = this.updateSummary();

  protected updateSummary() {
    const summary = this._items.reduce((summ, item) => {
      let discountedPrice = getDiscountedPrice(item.netPrice, item.discount) * item.quantity;
      const vatAmount = getVatAmount(discountedPrice, this._vat);
      const weight = item.weight * item.quantity;
      const price = getFinalPrice(discountedPrice, this._vat);

      return {
        netTotal: summ.netTotal + discountedPrice,
        totalVat: summ.totalVat +  vatAmount,
        totalWeight: summ.totalWeight + weight,
        totalPrice: summ.totalPrice + price
      };
    }, { netTotal: 0, totalVat: 0, totalWeight: 0, totalPrice: 0 });

    const transportFee = getTransportFee(summary.totalWeight);

    return {
      ...summary,
      transportFee
    }
  }
}

Come mai non cambia il valore anche se modifico la quanità? Perché sto andando a modificare la proprietà di un oggetto dell'array, ma l'array rimane lo stesso. Quindi non viene richiamato il set dell'input e quindi non vengono ricalcolati i dati. Non è un errore, in realtà è il mio AppComponent che sta facendo la cosa sbagliata aspettandosi che il componente si aggiorni anche senza cambiare l'input.

Per evitare questi problemi bisogna capire il concetto di mutable vs immutable objects e cercare di usare immutable object quando si lavora con gli input.

Un oggetto mutable è un oggetto le cui proprietà verranno modificate dopo la sua creazione, un oggetto immutable invece non viene modificato e ogni volta che devo modificarne una proprietà vado a creare una nuova istanza dell'oggetto.

Modificando l'istanza di un input e non solo le sue proprietà permette alla change-detection di angular di capire quali dati deve andare ad aggiornare. Nella pratica per aggiornare un componente con change-detection OnPush dall'esterno devo passargli un nuovo oggetto in input, modificare le proprietà di quello esistente non basta.

app.component.ts

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  items = CART;
  vat = getVat('IT');


  changeQuantity(item: any, newQuantity: number) {
    const index = this.items.indexOf(item);
    const clone = structuredClone(this.items);
    clone[index].quantity = newQuantity;
    this.items = clone;
  }
}

Ho introdotto un nuovo problema però, cambiando istanza dell'array items la mia lista viene svuotata e renderizzata di nuovo ogni volta che vado a modificare la quantità, facendo così l'utente perde il focus sul componente e deve ripremere ogni carattere che digita.

Per ovviare a questo problema angular mette a disposizione la direttiva NgForTrackBy, che permette di definire una funzione che determina se l'elemento di un ngFor è da renderizzare da 0 o solo da aggiornare.

app.component.html

<li class="list-group-item item" *ngFor="let item of items; trackBy: trackById">

app.component.ts

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

  trackById(_: number, item: any) {
    return item.id;
  }
}

Pipes

docs
In angular una pipe è una classe che permette di formattare un dato in stringa direttamente dal template. A differenza dei components (che creano nuovi elementi) le pipe vengono registrate a livello di applicazione e possono essere richiamate nei template, ad esempio:

<span>{{summary.netTotal | currency}}</span>

stampa: $2,998.90

Si possono cambiare il simbolo della valuta passando degli argomenti alla pipe, ad esempio:

<span>{{summary.netTotal | currency:'€'}}</span>

Ma si può anche configurare globalmente il comportamento della pipe, andando a modificare i providers dell'applicazione:
app.module.ts

...
import { registerLocaleData } from '@angular/common';
import localeIt from '@angular/common/locales/it';
registerLocaleData(localeIt);

@NgModule({
...
  providers: [
    {provide: DEFAULT_CURRENCY_CODE, useValue: '€'},
    {provide: LOCALE_ID, useValue: 'it-IT'}
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Il primo provider imposta il codice della valuta di default per l'applicazione.
Il secondo imposta il formato a seconda del codice della lingua (locale per l'esattezza), nel caso dell'Italia per le valute mettiamo prima il valore e poi il simbolo e separiamo le migliaia con il punto.

Custom pipes

Una pipe è una classe che implementa l'interfaccia PipeTransform e ha il decorator @Pipe.
L'interfaccia richiede l'implementazione di un singolo metodo transform che prende come argomenti il valore di partenza ed eventuali parametri e ritorna una stringa.

Un esempio di pipe è:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({name: 'exponentialStrength'})
export class ExponentialStrengthPipe implements PipeTransform {
  transform(value: number, exponent = 1): number {
    return Math.pow(value, exponent);
  }
}

Creiamo ora una pipe per rappresentare il valore dello sconto, in questo modo se dovessimo cambiare il formato basta andare ad agire in un unico file:

ng generate pipe pipes/discount-amount
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'discountAmount'
})
export class DiscountAmountPipe implements PipeTransform {

  transform(value: number): unknown {
    return value ? `(-${value.toFixed(2)})` : '';
  }

}

Ma ancora meglio possiamo prendere in prestito il comportamento CurrencyPipe, in questo modo applico automaticamente la configurazione globale:

import { CurrencyPipe } from '@angular/common';
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'discountAmount'
})
export class DiscountAmountPipe implements PipeTransform {

  constructor(private currency: CurrencyPipe) {}

  transform(value: number): unknown {
    const currencyString = this.currency.transform(value, '', '');
    return value ? `(-${currencyString})` : '';
  }

}

Services

Un service è tipicamente una classe con uno scopo definito e limitato. Sia service che component (e anche directives, pipes, ecc) vivono all'interno della logica di DI, la distinzione esiste per separare a livello logico quello che riguarda l'interfaccia e l'interazione con l'utente da quello che riguarda dati, e logica riutilizzabile tra i vari componenti.

Un componente può quindi delegare parte della logica ai servizi, come ad esempio:

  • recuperare dati
  • validare l'autenticazione
  • mettere a disposizione metodi specifici per la business logic
  • ecc

Un service che viene messo a disposizione dell'applicazione tramite la DI è una classe con il decorator @Injectable().

Creiamo un servizio che gestisce gli articoli

ng generate service services/cart-source
import { Injectable } from '@angular/core';

const CART = [
  ...
];

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

  getCart() {
    return [...CART];
  }
}

app.component.ts

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  items = this.cartSource.getCart();;
  vat = getVat('IT');

  constructor(private cartSource: CartSourceService) {}

...

}

Mettiamo per ipotesi che i dati del carrello non vengano da un array hardcoded ma dalle API di un server, o dal local-storage. In quel caso dobbiamo assicurarci che oltre a tornare i dati il servizio si occupi di salvare le modifiche.

Creiamo un metodo nel servizio per andare a modificare la quantità di un articolo.

cart-source.service.ts

@Injectable({
  providedIn: 'root'
})
export class CartSourceService {
  private items = [...CART];

  getCart() {
    return this.items;
  }

  setQuantity(id: number, quantity: number) {
    const index = this.items.findIndex(i => i.id === id);
    const clone = structuredClone(this.items);
    clone[index].quantity = quantity;
    this.items = clone;
  }
}

app.component.ts

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  items = this.cartSource.getCart();;
  vat = getVat('IT');

  constructor(private cartSource: CartSourceService) {}

  changeQuantity(item: any, newQuantity: number) {
    this.cartSource.setQuantity(item.id, newQuantity);
  }

  trackById(_: number, item: any) {
    return item.id;
  }
}

C'è un problema, l'interfaccia non si aggiorna più.
Questo è dovuto al fatto che AppComponent non richiama getCart() quando viene aggiornata la quantità, e quindi non sa che bisogna aggiornare i componenti.

Si potrebbe richiamare getCart dopo aver aggiornato la quantità, ma cosa succede se la lista viene aggiornata da un componente diverso? Come faccio a sapere che c'è un aggiornamento e che devo chiamare getCart() ?

Per risolvere questo problema in Angular si usano gli Observable.
Non sono stati creati da Angular, sono parte di una libreria chiamata rxjs.

Un Observable è un oggetto che mette a disposizione un metodo subscribe, registrando una funzione tramite questo metodo è possibile rimanere in ascolto dei nuovi valori che l'observable emette.

Andiamo a modificare il codice per vedere come si utilizzano.
cart-source.service.ts

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

const CART = [
  ...
];

@Injectable({
  providedIn: 'root'
})
export class CartSourceService {
  private items = [...CART];
  items$ = new Subject<any[]>();

  constructor() {
    this.items$.next(this.items);
  }

  setQuantity(id: number, quantity: number) {
    const index = this.items.findIndex(i => i.id === id);
    const clone = structuredClone(this.items);
    clone[index].quantity = quantity;
    this.items$.next(clone);
  }

  addArticle(article: any, quantity: number) {
    const clone = structuredClone(this.items);
    clone.push({
      ...article,
      quantity
    });
    this.items$.next(clone);
  }
}

app.component.ts

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  items$ = this.cartSource.items$;
  
  ...
}```

`app.component.html`
```html
<div class="container">
  <div id="items-list-container">
    <h3>I tuoi articoli</h3>
    <ul class="list-group" id="items-list">
      <li class="list-group-item item" *ngFor="let item of items$ | async; trackBy: trackById">
        <app-cart-item [item]="item" [vat]="vat" (quantityChange)="changeQuantity(item, $event)"></app-cart-item>
      </li>
    </ul>
  </div>

  <div class="mt-5">
    <h3>Totale ordine</h3>
    <div class="order-summary">
      <app-summary [items]="items$ | async" [vat]="vat"></app-summary>
    </div>
  </div>
</div>

C'è un problema, con questa implementazione la pagina non si aggiorna. Questo perché di default gli observable mandano solo i nuovi valori alle sottoscrizioni, se mi sottoscrivo a un observable vedrò solo i valori emessi da quel momento in poi.

Per risolvere questo problema ci sono diverse strategie:

  • usare del codice dentro ngOnInit per caricare i dati solo quando la pagina è stata creata (non elegante in questo caso)
  • usare un tipo di observable che mandi ad ogni nuova sottoscrizione l'ultimo valore emesso (ReplaySubject, BehaviourSubject, ecc)
  • se non è possibile modificare il codice del servizio si può trasformare qualsiasi observable tramite .pipe(shareReplay())

Codice corretto:
cart-source.service.ts


@Injectable({
  providedIn: 'root'
})
export class CartSourceService {
  private items = new BehaviorSubject<any[]>([...CART]);
  items$ = this.items.asObservable();

  setQuantity(id: number, quantity: number) {
    const index = this.items.value.findIndex(i => i.id === id);
    const clone = structuredClone(this.items.value);
    clone[index].quantity = quantity;
    this.items.next(clone);
  }

  addArticle(article: any, quantity: number) {
    const clone = structuredClone(this.items.value);
    clone.push({
      ...article,
      quantity
    });
    this.items.next(clone);
  }
}

async pipe è messa a disposizione da angular, quello che fa è sottoscriversi automaticamente all'observable e passare i nuovi valori all'input man mano che vengono emessi.

In questo caso c'è una sistemazione da fare nell'input di summary.component.ts perché nel caso non venga emesso nessun valore l'input viene settato a null. Bisogna fare in modo che il componente lo accetti, altrimenti la compilazione fallisce.

summary.component.ts

@Component({
  selector: 'app-summary',
  templateUrl: './summary.component.html',
  styleUrls: ['./summary.component.css'],
  changeDetection: ChangeDetectionStrategy.Default
})
export class SummaryComponent {
  @Input()
  set items(value: any[] | null) {
    if (!value) {
      value = [];
    }
    this._items = value;
    this.summary = this.updateSummary();
  }
  ...
}

Testiamo il funzionamento dell'observable mettendoci manualmente in ascolto.

app.component.ts

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  ...
  
  ngOnInit(): void {
      this.cartSource.items$.subscribe((items) => {
        console.log(items);
      });
  }

  ...
}

Esercizio

Provare a sviluppare un servizio per l'IVA, questo servizio deve mettere a disposizione un observable iva$ e un metodo setCountry(countryCode: string) che va a calcolare l'IVA a seconda del paese ed emette il nuovo valore tramite l'observable.
Andare anche a modificare il resto del codice perché utilizzi il nuovo servizio.

Soluzione

vat.service.ts

import { Injectable } from '@angular/core';
import { ReplaySubject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class VatService {
  vat$ = new ReplaySubject<number>();

  constructor() {
    this.vat$.next(0);
  }

  setCountry(countryCode: string) {
    const value = countryCode === 'IT' ? 0.22 : 0;
    this.vat$.next(value);
  }
}