HttpClient, lær hvad en Observable er, og brug toSignal() til at bygge bro mellem Observables og Signals.
Lige nu er alle emissionsfaktorer hardcoded inde i Emission servicen. I den virkelige verden kommer data næsten altid fra en server. Til det bruger Angular HttpClient, som er Angulars indbyggede måde at lave HTTP requests på (GET, POST, PUT osv.).
Når du kalder http.get(), får du ikke data med det samme. Du får i stedet en Observable. Tænk på det som et løfte om data der kommer på et tidspunkt. En Observable er som en avis-abonnement: du tilmelder dig (subscribe), og når avisen er klar, bliver den leveret til dig.
Det smarte er at Angular har en funktion kaldet toSignal() der kan konvertere en Observable til et Signal. Så får du det bedste fra begge verdener: HttpClient til at hente data, og Signals til at reagere på ændringer i din template.
Checkout tag module-7-start. Servicen har en hardcoded herdCards array. Vi skal flytte den data ud i en JSON-fil og hente den med HttpClient.
git checkout module-7-start
Opret filen public/emission-factors.json med alle emissionsfaktorerne. Filer i public/ mappen bliver serveret direkte af dev-serveren, så vi kan hente dem via HTTP:
// public/emission-factors.json
[
{ "key": "dairyCows", "label": "Malkekøer", "emoji": "🐄", "factor": 3000 },
{ "key": "beefCows", "label": "Kødkvæg", "emoji": "🐂", "factor": 2400 },
{ "key": "calves", "label": "Kalve & Ungdyr", "emoji": "🐃", "factor": 1000 },
{ "key": "pigs", "label": "Slagtesvin", "emoji": "🐖", "factor": 50 },
{ "key": "sows", "label": "Søer", "emoji": "🐗", "factor": 450 },
{ "key": "chickens", "label": "Slagtekyllinger", "emoji": "🐓", "factor": 1.5 }
]
Før du kan bruge HttpClient, skal Angular vide at den skal stille den til rådighed. Åbn app.config.ts og tilføj provideHttpClient(withFetch()):
// app.config.ts
import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
import { provideHttpClient, withFetch } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideZonelessChangeDetection(),
provideHttpClient(withFetch()),
]
};
provideHttpClient() registrerer HttpClient i Angulars DI-system. withFetch() fortæller Angular at den skal bruge browserens moderne fetch() API under motorhjelmen (i stedet for det ældre XMLHttpRequest).
Nu skal vi ændre servicen så den henter data fra JSON-filen i stedet for at have den hardcoded. Her sker det interessante:
inject()http.get() — det returnerer en ObservabletoSignal() — så det passer ind i vores signal-baserede arkitektur// calculator/emission.ts
import { Injectable, inject, signal, computed } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';
// ... interfaces uændrede ...
@Injectable({ providedIn: 'root' })
export class Emission {
private readonly http = inject(HttpClient);
readonly herdCards = toSignal(
this.http.get<Card[]>('/emission-factors.json'),
{ initialValue: [] }
);
// ... resten uændret ...
}
Lad os forstå hvad der sker:
http.get<Card[]>('/emission-factors.json') laver en GET request og returnerer en Observable der på et tidspunkt leverer et array af Card-objektertoSignal() konverterer den Observable til et Signal. Det er broen mellem Observables og Signals{ initialValue: [] } siger: "Mens vi venter på data, brug et tomt array." Uden dette ville signalet starte som undefinedherdCards nu er et Signal (ikke et almindeligt array), skal du kalde det med () de steder det bruges. F.eks. i totalEmission computed: this.herdCards() i stedet for this.herdCards.
Opdater også totalEmission så den kalder herdCards() som et signal:
readonly totalEmission = computed(() => {
const counts = this.herdCounts();
const cards = this.herdCards(); // ← kald som signal!
return cards.reduce(
(sum, c) => sum + counts[c.key] * c.factor, 0
) / 1000;
});
Fordi herdCards nu er et Signal, skal du også opdatere HerdInput. I templaten skal du kalde signalet med ():
// I herd-input.ts template
@for (card of herdCards(); track card.key) {
<app-herd-card
[herd]="card"
(countChanged)="onCountChanged(card.key, $event)" />
}
Start dev-serveren og åbn appen i browseren:
ng serve
Appen skal se ud præcis som før. Men åbn DevTools → Network tab og reload siden. Nu kan du se en HTTP request til /emission-factors.json. Dataen kommer fra serveren i stedet for at være hardcoded!
public/emission-factors.json og ændr en faktor (f.eks. sæt dairyCows til 9999). Reload browseren og se at beregningen ændrer sig. Dataen er nu ekstern og kan ændres uden at kompilere appen igen!
Hvad sker der hvis serveren ikke kan levere data? Lad os finde ud af det:
public/emission-factors.jsonAppen crasher stille og viser ingen kort. Det er dårlig brugeroplevelse. Lad os tilføje fejlhåndtering med catchError:
// calculator/emission.ts
import { catchError } from 'rxjs/operators';
import { of } from 'rxjs';
// I Emission klassen:
readonly error = signal<string | null>(null);
readonly herdCards = toSignal(
this.http.get<Card[]>('/emission-factors.json').pipe(
catchError(() => {
this.error.set('Kunne ikke hente emissionsdata');
return of([] as Card[]);
})
),
{ initialValue: [] }
);
Her bruger vi pipe() til at tilføje operatorer på Observablen:
catchError() fanger fejl (som 404) og lader os reagereof([]) opretter en ny Observable der straks leverer et tomt array som fallbackerror signal som UI'en kan viseNu kan du vise fejlbeskeden i f.eks. HerdInput templaten:
// herd-input.ts template
@if (emissionService.error()) {
<p class="error">⚠️ {{ emissionService.error() }}</p>
}
@for (card of herdCards(); track card.key) { ... }
emissionService.error() i templaten skal propertyen ikke være private. Skift den til readonly (uden private) eller opret en lokal reference.
Opret public/emission-factors.json igen (kopier fra trin 1) og reload appen. Alt skal virke som normalt, og fejlbeskeden forsvinder.
app.config.ts før HttpClient kan brugesimport { Injectable, inject, signal, computed } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';
import { catchError } from 'rxjs/operators';
import { of } from 'rxjs';
export interface HerdCounts {
dairyCows: number;
beefCows: number;
calves: number;
pigs: number;
sows: number;
chickens: number;
}
export interface Card {
key: keyof HerdCounts;
label: string;
emoji: string;
factor: number;
}
@Injectable({ providedIn: 'root' })
export class Emission {
private readonly http = inject(HttpClient);
readonly error = signal<string | null>(null);
readonly herdCards = toSignal(
this.http.get<Card[]>('/emission-factors.json').pipe(
catchError(() => {
this.error.set('Kunne ikke hente emissionsdata');
return of([] as Card[]);
})
),
{ initialValue: [] }
);
readonly herdCounts = signal<HerdCounts>({
dairyCows: 0,
beefCows: 0,
calves: 0,
pigs: 0,
sows: 0,
chickens: 0,
});
readonly totalEmission = computed(() => {
const counts = this.herdCounts();
const cards = this.herdCards();
return cards.reduce(
(sum, c) => sum + counts[c.key] * c.factor, 0
) / 1000;
});
updateCount(key: keyof HerdCounts, count: number) {
this.herdCounts.update(prev => ({ ...prev, [key]: count }));
}
}
import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
import { provideHttpClient, withFetch } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideZonelessChangeDetection(),
provideHttpClient(withFetch()),
]
};
import { Component, inject } from '@angular/core';
import { HerdCard } from './herd-card/herd-card';
import { Emission, HerdCounts } from '../emission';
@Component({
selector: 'app-herd-input',
imports: [HerdCard],
template: `
@if (emissionService.error()) {
<p class="error">⚠️ {{ emissionService.error() }}</p>
}
@for (card of herdCards(); track card.key) {
<app-herd-card
[herd]="card"
(countChanged)="onCountChanged(card.key, $event)" />
}
`,
})
export class HerdInput {
readonly emissionService = inject(Emission);
readonly herdCards = this.emissionService.herdCards;
onCountChanged(key: keyof HerdCounts, count: number) {
this.emissionService.updateCount(key, count);
}
}