Checkout tag module-5-start. Alle components bruger klassiske @Input()/@Output() decorators og EventEmitter.
git checkout module-5-start
Lige nu ser herd-card.ts sådan ud:
// herd-card.ts (udgangspunkt)
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Card } from '../../shared/card';
export class HerdCard {
@Input({ required: true }) herd!: Card;
@Output() countChanged = new EventEmitter<number>();
// ...
}
Og herd-input.ts har manuelle beregninger med @Output() og EventEmitter:
// herd-input.ts (udgangspunkt)
import { Component, Output, EventEmitter } from '@angular/core';
export class HerdInput {
@Output() totalEmissionChange = new EventEmitter<{ totalEmission: number }>();
herdCounts: HerdCounts = { ... };
// ...
}
Et signal er en reaktiv værdi. Tænk på det som en variabel der "ved" hvem der bruger den. Når signalet ændrer sig, opdaterer Angular automatisk alle steder der læser det. Der findes tre typer:
signal(value) — skrivbart state. Du kan læse det med mySignal() og opdatere det med .set() eller .update().computed(() => ...) — afledt state der automatisk genberegnes når de signals den afhænger af ændrer sig. Readonly.effect(() => ...) — kører en side effect (f.eks. logging) hver gang et signal i den ændrer sig.Angular har også signal baserede versioner af input() og output() der erstatter de gamle @Input()/@Output() decorators.
Angular CLI har en built-in migration der konverterer alle @Input() decorators til den nye input() funktion automatisk:
ng generate @angular/core:signal-input-migration
Når CLI'en spørger, vælg ./ som sti og "Yes" til at migrere så meget som muligt. Migrationen finder 2 inputs og konverterer dem:
// herd-card.ts — FØR
@Input({ required: true }) herd!: Card;
// herd-card.ts — EFTER
readonly herd = input.required<Card>();
// result-panel.ts — FØR
@Input() totalEmission = 0;
// result-panel.ts — EFTER
readonly totalEmission = input(0);
Migrationen opdaterer også templates automatisk. Da input() returnerer et signal, skal du kalde det som en funktion for at læse værdien. Så herd.emoji bliver til herd().emoji, og totalEmission bliver til totalEmission().
readonly? Signal inputs sættes af parent via template binding. Du kan ikke overskrive selve signalet inde i componenten, kun læse det. readonly gør dette tydeligt i koden.
Næste migration konverterer @Output() og EventEmitter til den nye output() funktion:
ng generate @angular/core:output-migration
Det ændrer begge outputs i projektet:
// herd-card.ts — FØR
@Output() countChanged = new EventEmitter<number>();
// herd-card.ts — EFTER
readonly countChanged = output<number>();
// herd-input.ts — FØR
@Output() totalEmissionChange = new EventEmitter<{ totalEmission: number }>();
// herd-input.ts — EFTER
readonly totalEmissionChange = output<{ totalEmission: number }>();
Importen ændres fra EventEmitter, Output til blot output (lille bogstav, en funktion i stedet for en dekorator). Du kalder stadig .emit() på præcis samme måde som før.
Nu tilføjer vi den virkelige magi. I herd-input.ts konverterer vi herdCounts fra en almindelig property til et signal:
// herd-input.ts
import { Component, signal, computed, effect, output } from '@angular/core';
readonly herdCounts = signal<HerdCounts>({
dairyCows: 0,
beefCows: 0,
calves: 0,
pigs: 0,
sows: 0,
chickens: 0,
});
signal() opretter en reaktiv værdi. Du læser den med this.herdCounts() og opdaterer den med .set() eller .update().
computed() opretter et readonly signal der automatisk genberegnes når de signals det afhænger af ændrer sig. Erstat den manuelle calculateTotal() metode med:
readonly totalEmission = computed(() => {
const counts = this.herdCounts();
return this.herdCards.reduce(
(sum, card) => sum + (counts[card.key] * card.factor), 0
) || 0;
});
Når herdCounts signalet ændrer sig, genberegner Angular automatisk totalEmission. Du behøver aldrig kalde en beregningsmetode manuelt igen.
effect() kører en side effect hver gang et signal den læser ændrer sig. Tilføj den i constructoren:
constructor() {
effect(() => console.log('Total emission:', this.totalEmission()));
}
Angular registrerer automatisk at denne effect afhænger af totalEmission (som afhænger af herdCounts). Åbn browser console og se loggen opdatere sig hver gang du ændrer et antal.
Da herdCounts nu er et signal, kan du ikke bare tildele direkte med =. Brug .update() der tager den nuværende værdi og returnerer en ny:
onCountChanged(key: keyof HerdCounts, count: number): void {
this.herdCounts.update(prev => ({ ...prev, [key]: count }));
this.totalEmissionChange.emit({ totalEmission: this.totalEmission() / 1000 });
}
.update() modtager en funktion der får den gamle værdi (prev) og returnerer en ny. Spread operatoren (...prev) kopierer alle eksisterende værdier og overskriver kun den ændrede nøgle.
calculateTotal() metode er helt væk. computed() klarer alt automatisk. Når herdCounts opdateres, ved totalEmission at den skal genberegnes, og effect() ved at den skal logge igen. Det er hele pointen med signals: du beskriver hvad der afhænger af hvad, og Angular klarer hvornår.
import { Component, input, output } from '@angular/core';
import { Card } from '../../shared/card';
import { DecimalPipe } from '@angular/common';
@Component({
selector: 'app-herd-card',
imports: [DecimalPipe],
templateUrl: './herd-card.html',
styleUrl: './herd-card.css',
})
export class HerdCard {
readonly herd = input.required<Card>();
readonly countChanged = output<number>();
isActive = false;
onCountChanged(count: number) {
this.isActive = count > 0;
this.countChanged.emit(count);
}
}
import { Component, signal, computed, effect, output } from '@angular/core';
import { Card, HerdCounts, herdCards } from '../../shared/herd-data';
import { HerdCard } from '../herd-card/herd-card';
@Component({
selector: 'app-herd-input',
imports: [HerdCard],
templateUrl: './herd-input.html',
styleUrl: './herd-input.css',
})
export class HerdInput {
readonly herdCards: Card[] = herdCards;
readonly totalEmissionChange = output<{ totalEmission: number }>();
readonly herdCounts = signal<HerdCounts>({
dairyCows: 0,
beefCows: 0,
calves: 0,
pigs: 0,
sows: 0,
chickens: 0,
});
readonly totalEmission = computed(() => {
const counts = this.herdCounts();
return this.herdCards.reduce(
(sum, card) => sum + (counts[card.key] * card.factor), 0
) || 0;
});
constructor() {
effect(() => console.log('Total emission:', this.totalEmission()));
}
onCountChanged(key: keyof HerdCounts, count: number): void {
this.herdCounts.update(prev => ({ ...prev, [key]: count }));
this.totalEmissionChange.emit({ totalEmission: this.totalEmission() / 1000 });
}
}
import { Component, input } from '@angular/core';
import { DecimalPipe } from '@angular/common';
@Component({
selector: 'app-result-panel',
imports: [DecimalPipe],
templateUrl: './result-panel.html',
styleUrl: './result-panel.css',
})
export class ResultPanel {
readonly totalEmission = input(0);
}