[Angular v16] Signals
Service:
fromObservable & fromSignal
can transform observable to and from signals.
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Injectable, signal } from '@angular/core';
import {
catchError,
filter,
forkJoin,
map,
Observable,
shareReplay,
switchMap,
throwError
} from 'rxjs';
import { fromObservable, fromSignal } from '@angular/core/rxjs-interop';
import { Film, Vehicle, VehicleResponse } from './vehicle';
@Injectable({
providedIn: 'root'
})
export class VehicleService {
private url = 'https://swapi.py4e.com/api/vehicles';
// First page of vehicles
// If the price is empty, randomly assign a price
// (We can't modify the backend in this demo)
private vehicles$ = this.http.get<VehicleResponse>(this.url).pipe(
map((data) =>
data.results.map((v) => ({
...v,
cost_in_credits: isNaN(Number(v.cost_in_credits))
? String(Math.random() * 100000)
: v.cost_in_credits,
}) as Vehicle)
),
shareReplay(1),
catchError(this.handleError)
);
// Expose signals from this service
vehicles = fromObservable<Vehicle[], Vehicle[]>(this.vehicles$, []);
selectedVehicle = signal<Vehicle | undefined>(undefined);
private vehicleFilms$ = fromSignal(this.selectedVehicle).pipe(
filter(Boolean),
switchMap(vehicle =>
forkJoin(vehicle.films.map(link =>
this.http.get<Film>(link)))
)
);
vehicleFilms = fromObservable<Film[], Film[]>(this.vehicleFilms$, []);
constructor(private http: HttpClient) {
}
vehicleSelected(vehicleName: string) {
const foundVehicle = this.vehicles().find((v) => v.name === vehicleName);
this.selectedVehicle.set(foundVehicle);
}
private handleError(err: HttpErrorResponse): Observable<never> {
// in a real world app, we may send the server to some remote logging infrastructure
// instead of just logging it to the console
let errorMessage = '';
if (err.error instanceof ErrorEvent) {
// A client-side or network error occurred. Handle it accordingly.
errorMessage = `An error occurred: ${err.error.message}`;
} else {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong,
errorMessage = `Server returned code: ${err.status}, error message is: ${err.message
}`;
}
console.error(errorMessage);
return throwError(() => errorMessage);
}
}
Component:
Only need to refer the signals in service:
import { Component } from '@angular/core';
import { NgFor, NgClass, NgIf, AsyncPipe } from '@angular/common';
import { VehicleService } from '../vehicle.service';
@Component({
selector: 'sw-vehicle-list',
standalone: true,
imports: [AsyncPipe, NgClass, NgFor, NgIf],
templateUrl: './vehicle-list.component.html'
})
export class VehicleListComponent {
pageTitle = 'Vehicles';
errorMessage = '';
// Component signals
vehicles = this.vehicleService.vehicles;
selectedVehicle = this.vehicleService.selectedVehicle;
constructor(private vehicleService: VehicleService) { }
// When a vehicle is selected, emit the selected vehicle name
onSelected(vehicleName: string): void {
this.vehicleService.vehicleSelected(vehicleName);
}
}
Template:
<div class="card">
<div class="card-header">
{{pageTitle}}
</div>
<div class='card-body'
*ngIf="vehicles().length">
<div class="list-group">
<button type="button"
class="list-group-item"
*ngFor="let vehicle of vehicles()"
[ngClass]="{'active': vehicle?.name === selectedVehicle()?.name}"
(click)="onSelected(vehicle.name)">
{{ vehicle.name }}
</button>
</div>
</div>
<div class="alert alert-danger"
*ngIf="errorMessage">
{{errorMessage }}
</div>
</div>