[Angular v16] Signals

发布时间 2023-04-13 14:06:28作者: Zhentiw

Service:

fromObservable & fromSignalcan 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>