@Output() og EventEmitter til at sende data fra child components op til parent components. Byg en komplet dataflow fra HerdCard helt op til ResultPanel, så appen beregner total CO₂ udledning.
Checkout tag module-4-start. HerdCard modtager allerede et herd objekt via @Input({ required: true }) og viser data med interpolation. Input-feltet har en setActive metode der kun styrer lokal styling. Ændringer i input-feltet sender altså ikke data nogen steder hen.
git checkout module-4-start
Lige nu ser herd-card.ts sådan ud:
// herd-card.ts (udgangspunkt)
import { Component, Input } from '@angular/core';
import { Card } from '../shared/herd-data';
export class HerdCard {
@Input({ required: true }) herd!: Card;
isActive = false;
setActive(count: number) {
this.isActive = count > 0;
}
}
Og i herd-card.html kalder input-feltet setActive:
<input type="number" min="0" placeholder="0" #input
(input)="setActive(input.valueAsNumber)" />
Problemet: værdien fra input-feltet bliver aldrig sendt til parent. Vi skal bruge @Output() til at kommunikere opad.
I Angular flyder data i én retning. @Input() sender data ned fra parent til child. @Output() sender data op fra child til parent.
EventEmitter er den klasse du bruger til at "fyre" en event. Tænk på det som en knap der sender en besked til den component der lytter. Du opretter en EventEmitter med @Output() dekoratoren, og parent kan så lytte med den velkendte (eventNavn) syntaks fra event binding.
Importér Output og EventEmitter fra @angular/core. Tilføj en countChanged output og omdøb setActive til onCountChanged:
// herd-card.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Card } from '../shared/herd-data';
export class HerdCard {
@Input({ required: true }) herd!: Card;
@Output() countChanged = new EventEmitter<number>();
isActive = false;
onCountChanged(count: number) {
this.isActive = count > 0;
this.countChanged.emit(count);
}
}
EventEmitter<number> angiver at eventen sender et tal med. .emit(count) fyrer eventen og sender værdien op til den parent der lytter.
Skift metodekaldet i template fra setActive til onCountChanged:
<!-- herd-card.html -->
<input type="number" min="0" placeholder="0" #input
(input)="onCountChanged(input.valueAsNumber)" />
#input er en template reference variable der giver direkte adgang til DOM elementet, så vi kan kalde input.valueAsNumber uden at gå via $event.
I herd-input.html tilføjer du output binding med runde parenteser, præcis ligesom du lytter på DOM events:
<!-- herd-input.html -->
@for(herd of herdCards; track herd.key) {
<app-herd-card
[herd]="herd"
(countChanged)="onCountChanged(herd.key, $event)"
/>
}
$event indeholder den værdi der blev emitted, altså tallet fra input-feltet. Vi sender også herd.key med, så parent ved hvilket dyr der blev ændret.
Nu skal HerdInput holde styr på antal af hvert dyr, beregne total udledning og sende resultatet videre opad med sin egen @Output():
// herd-input.ts
import { Component, Output, EventEmitter } from '@angular/core';
import { Card, HerdCounts, herdCards } from '../shared/herd-data';
export class HerdInput {
readonly herdCards: Card[] = herdCards;
@Output() totalEmissionChange = new EventEmitter<{ totalEmission: number }>();
herdCounts: HerdCounts = {
dairyCows: 0,
beefCows: 0,
calves: 0,
pigs: 0,
sows: 0,
chickens: 0,
};
onCountChanged(key: keyof HerdCounts, count: number) {
this.herdCounts[key] = count;
this.calculateTotal();
}
calculateTotal() {
const totalEmission = this.herdCards.reduce(
(sum, card) => sum + this.herdCounts[card.key] * card.factor,
0
) / 1000;
this.totalEmissionChange.emit({ totalEmission });
}
}
reduce summerer antal × faktor for hvert dyr og dividerer med 1000 for at konvertere fra kg til ton.
Calculator er parent til både HerdInput og ResultPanel. Den lytter på totalEmissionChange fra HerdInput og sender resultatet ned til ResultPanel:
// calculator.ts
export class Calculator {
totalEmission = 0;
onTotalEmissionChange(event: { totalEmission: number }) {
this.totalEmission = event.totalEmission;
}
}
I calculator template, bind det sammen:
<!-- calculator.html -->
<app-herd-input
(totalEmissionChange)="onTotalEmissionChange($event)"
/>
<app-result-panel
[totalEmission]="totalEmission"
/>
Tilføj en @Input() til ResultPanel og vis værdien formateret:
// result-panel.ts
import { Component, Input } from '@angular/core';
export class ResultPanel {
@Input() totalEmission = 0;
}
I result-panel template:
{{ totalEmission | number:'1.1-1' }} ton/år
number:'1.1-1' er en pipe der formaterer til mindst 1 decimal og højst 1 decimal (f.eks. 12.3).
herd-card → (countChanged) → herd-input → (totalEmissionChange) → calculator → [totalEmission] → result-panel@Output() events og nedad via @Input() property binding. Denne ensrettede datastrøm gør det let at følge hvor data kommer fra og hvor den ender.
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import { Card } from '../shared/herd-data';
@Component({
selector: 'app-herd-card',
imports: [DecimalPipe],
templateUrl: './herd-card.html',
styleUrl: './herd-card.css',
})
export class HerdCard {
@Input({ required: true }) herd!: Card;
@Output() countChanged = new EventEmitter<number>();
isActive = false;
onCountChanged(count: number) {
this.isActive = count > 0;
this.countChanged.emit(count);
}
}
import { Component, Output, EventEmitter } 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;
@Output() totalEmissionChange = new EventEmitter<{ totalEmission: number }>();
herdCounts: HerdCounts = {
dairyCows: 0,
beefCows: 0,
calves: 0,
pigs: 0,
sows: 0,
chickens: 0,
};
onCountChanged(key: keyof HerdCounts, count: number) {
this.herdCounts[key] = count;
this.calculateTotal();
}
calculateTotal() {
const totalEmission = this.herdCards.reduce(
(sum, card) => sum + this.herdCounts[card.key] * card.factor,
0
) / 1000;
this.totalEmissionChange.emit({ totalEmission });
}
}
import { Component } from '@angular/core';
import { HerdInput } from '../herd-input/herd-input';
import { ResultPanel } from '../result-panel/result-panel';
@Component({
selector: 'app-calculator',
imports: [HerdInput, ResultPanel],
templateUrl: './calculator.html',
styleUrl: './calculator.css',
})
export class Calculator {
totalEmission = 0;
onTotalEmissionChange(event: { totalEmission: number }) {
this.totalEmission = event.totalEmission;
}
}