Emission service, flyt al state og beregningslogik derhen, og brug inject() så begge components deler det samme data uden @Input()/@Output(). Derefter udvider vi servicen med fuld afgiftsberegning og dynamisk result panel.
Lige nu bor al logik og state inde i dine components. Det fungerer, men der er et problem: HerdInput og ResultPanel er siblings (søskende), og den eneste måde de kan dele data på er ved at sende det op til Calculator og ned igen via @Input()/@Output().
En service er en klasse der lever udenfor components. Den kan holde på data og logik som flere components skal bruge. Angular sørger for at alle components der beder om den samme service, får den samme instans. Det kalder man en singleton.
Angular bruger et system kaldet Dependency Injection (DI) til at levere services. I stedet for at du selv opretter en instans med new, beder du Angular om at give dig den med inject(). Læs mere i DI guiden.
Checkout tag module-6-start. Al beregningslogik og state bor i components, og data sendes op og ned via @Input()/@Output().
git checkout module-6-start
Kør denne kommando i terminalen:
ng generate service calculator/emission
Det opretter to filer i calculator/ mappen:
emission.ts — selve servicenemission.spec.ts — testfil (den rører vi ikke)Den genererede fil ser sådan ud:
// calculator/emission.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class Emission {}
@Injectable({ providedIn: 'root' }) fortæller Angular: "Opret én enkelt instans af denne klasse og del den med alle der beder om den." Det er det der gør den til en singleton.
Lige nu ligger Card og HerdCounts interfaces i shared/ mappen. Flyt dem ind i toppen af emission.ts (over klassen). Derefter kan du slette hele shared/ mappen.
// calculator/emission.ts
import { Injectable, signal, computed } from '@angular/core';
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;
}
shared/ mappen, skal du også opdatere imports i andre filer (f.eks. herd-card.ts) så de peger på '../emission' i stedet for '../../shared/card'.
Nu flytter vi alt det interessante ind i servicen: kortene, tællingerne og beregningen. Det er præcis den samme logik som før, bare samlet ét sted.
// calculator/emission.ts
@Injectable({ providedIn: 'root' })
export class Emission {
readonly herdCards: Card[] = [
{ 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 },
];
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, c) => sum + counts[c.key] * c.factor, 0
) / 1000;
});
updateCount(key: keyof HerdCounts, count: number) {
this.herdCounts.update(prev => ({ ...prev, [key]: count }));
}
}
Læg mærke til / 1000 i totalEmission. Den konverterer fra kg til ton direkte i beregningen.
Nu kan HerdInput blive meget enklere. Al state og logik bor i servicen, så component'en skal bare:
inject()herdCards til templatenupdateCount() når brugeren ændrer et talFjern @Output() — den er ikke nødvendig længere! Data flyder ikke længere op gennem parent. I stedet skriver begge components direkte til den delte service.
// calculator/herd-input/herd-input.ts
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],
templateUrl: './herd-input.html',
styleUrl: './herd-input.css',
})
export class HerdInput {
private readonly emissionService = inject(Emission);
readonly herdCards = this.emissionService.herdCards;
onCountChanged(key: keyof HerdCounts, count: number) {
this.emissionService.updateCount(key, count);
}
}
Samme idé. ResultPanel behøver ikke længere en @Input() — den henter bare totalEmission direkte fra servicen:
// calculator/result-panel/result-panel.ts
import { Component, inject } from '@angular/core';
import { Emission } from '../emission';
@Component({
selector: 'app-result-panel',
template: `
<div class="result">
<h2>Total: {{ totalEmission() }} ton CO₂e</h2>
</div>
`,
})
export class ResultPanel {
private readonly emissionService = inject(Emission);
readonly totalEmission = this.emissionService.totalEmission;
}
Til sidst kan Calculator templaten ryddes op. Du behøver ikke længere sende data mellem children via bindings:
// calculator/calculator.ts
@Component({
selector: 'app-calculator',
imports: [HerdInput, ResultPanel],
template: `
<div class="layout">
<app-herd-input />
<app-result-panel />
</div>
`,
})
export class Calculator {}
Ingen [totalEmission] binding, ingen (totalEmissionChange) event. Begge children taler direkte med servicen.
HerdInput → Calculator → ResultPanel. Nu taler begge components direkte med den delte Emission service. Det er enklere, og det skalerer bedre når appen vokser.
Servicen virker nu, men den viser kun et enkelt tal. Den rigtige klimaberegner skal også beregne afgifter, vise en fordeling per dyretype, og sammenligne afgifterne for 2030 vs 2035. Alt det kan vi bygge med computed() signals.
computed() er som en formel i et regneark: du definerer beregningen én gang, og Angular opdaterer resultatet automatisk hver gang de underliggende værdier ændres. Du behøver aldrig manuelt kalde "genberegn".
Tilføj disse to interfaces i toppen af emission.ts, sammen med de eksisterende:
export interface EmissionResult {
animalType: string;
count: number;
emissionFactor: number;
totalKgCO2e: number;
percentage: number;
}
export interface TaxResult {
year: 2030 | 2035;
marginalRate: number;
deductionPercent: number;
effectiveRate: number;
totalTax: number;
}
EmissionResult beskriver én dyretypes bidrag til den samlede udledning. TaxResult indeholder alle afgiftstal for et givet år.
Tilføj denne konstant over klassen i emission.ts. Den indeholder de politisk vedtagne afgiftssatser fra "Aftale om Grønt Danmark":
const TAX_RATES = {
2030: { marginal: 300, deduction: 0.6 },
2035: { marginal: 750, deduction: 0.6 },
} as const;
I 2030 er den marginale sats 300 kr/ton CO₂e med 60% bundfradrag. I 2035 stiger satsen til 750 kr/ton. as const gør at TypeScript kender de præcise værdier.
Nu tilføjer vi de avancerede beregninger inde i Emission klassen. Tilføj disse felter efter det eksisterende totalEmission:
// Nyt signal: hvilket år beregner vi afgift for?
readonly selectedYear = signal<2030 | 2035>(2030);
// Total i kg (bruges internt af andre computed)
readonly totalEmissionsKg = computed(() => {
const counts = this.herdCounts();
return this.herdCards.reduce(
(sum, c) => sum + counts[c.key] * c.factor, 0
);
});
Bemærk: totalEmission (i ton) kan nu bruge totalEmissionsKg:
readonly totalEmission = computed(() => this.totalEmissionsKg() / 1000);
Tilføj derefter fordelingen per dyretype:
// Fordeling: hvor meget bidrager hver dyretype?
readonly emissionBreakdown = computed<EmissionResult[]>(() => {
const counts = this.herdCounts();
const totalKg = this.totalEmissionsKg();
return this.herdCards
.map(card => ({
animalType: card.label,
count: counts[card.key],
emissionFactor: card.factor,
totalKgCO2e: counts[card.key] * card.factor,
percentage: totalKg > 0
? (counts[card.key] * card.factor / totalKg) * 100
: 0,
}))
.filter(item => item.count > 0);
});
Og afgiftsberegningen for det valgte år:
// Afgift for det valgte år
readonly taxResult = computed<TaxResult>(() => {
const year = this.selectedYear();
const tons = this.totalEmission();
const rates = TAX_RATES[year];
const effectiveRate = rates.marginal * (1 - rates.deduction);
return {
year,
marginalRate: rates.marginal,
deductionPercent: rates.deduction * 100,
effectiveRate,
totalTax: tons * effectiveRate,
};
});
Til sidst en sammenligning mellem de to år:
// Sammenlign 2030 vs 2035
readonly taxComparison = computed(() => {
const tons = this.totalEmission();
const tax2030 = tons * (TAX_RATES[2030].marginal * (1 - TAX_RATES[2030].deduction));
const tax2035 = tons * (TAX_RATES[2035].marginal * (1 - TAX_RATES[2035].deduction));
return {
tax2030,
tax2035,
difference: tax2035 - tax2030,
percentIncrease: tax2030 > 0
? ((tax2035 - tax2030) / tax2030) * 100
: 0,
};
});
computed() signals kan bygge på hinanden. totalEmission bruger totalEmissionsKg, og taxResult bruger totalEmission. Når brugeren ændrer et dyreantal, opdateres hele kæden automatisk. Det er kraften i reaktiv programmering!
Tilføj disse to metoder i Emission klassen, efter updateCount:
setYear(year: 2030 | 2035) {
this.selectedYear.set(year);
}
reset() {
this.herdCounts.set({
dairyCows: 0,
beefCows: 0,
calves: 0,
pigs: 0,
sows: 0,
chickens: 0,
});
}
setYear() skifter det valgte år, og alle afgiftsberegninger opdateres automatisk. reset() nulstiller alle dyreantal til 0.
Templaten bruger Angular's DecimalPipe med dansk formatering: | number: '1.1-1' : 'da'. For at det virker, skal Angular kende det danske locale. Åbn app.config.ts og tilføj:
// app.config.ts
import { ApplicationConfig, provideBrowserGlobalErrorListeners, LOCALE_ID } from '@angular/core';
import localeDa from '@angular/common/locales/da';
import { registerLocaleData } from '@angular/common';
registerLocaleData(localeDa);
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
{ provide: LOCALE_ID, useValue: 'da' }
]
};
registerLocaleData() gør at Angular kender danske talformater (komma som decimalseparator, punktum som tusindtalsseparator). LOCALE_ID sætter det som default for hele appen. Læs mere i locale-guiden.
Nu skal ResultPanel vise alle de nye beregninger. Opdater component'en til at eksponere de nye signals og tilføje hjælpemetoder. Bemærk at vi bruger templateUrl til en separat HTML-fil:
// calculator/result-panel/result-panel.ts
import { DecimalPipe } from '@angular/common';
import { Component, inject } from '@angular/core';
import { Emission } from '../emission';
@Component({
selector: 'app-result-panel',
imports: [DecimalPipe],
templateUrl: './result-panel.html',
styleUrl: './result-panel.css',
})
export class ResultPanel {
private readonly emissionService = inject(Emission);
readonly selectedYear = this.emissionService.selectedYear;
readonly totalEmission = this.emissionService.totalEmission;
readonly taxResult = this.emissionService.taxResult;
readonly comparison = this.emissionService.taxComparison;
readonly breakdown = this.emissionService.emissionBreakdown;
setYear(year: 2030 | 2035) {
this.emissionService.setYear(year);
}
formatDanish(value: number): string {
return Math.round(value).toLocaleString('da-DK');
}
}
Og her er templaten (result-panel.html) med fuld afgiftsvisning. Bemærk | number: '1.1-1' : 'da' — den bruger Angular's DecimalPipe med dansk locale til korrekt talformatering (komma som decimalseparator):
<!-- calculator/result-panel/result-panel.html -->
<div class="panel">
<div class="emission-hero">
<h3>Total CO2e-udledning</h3>
<p>{{ totalEmission() | number: '1.1-1' : 'da' }} <span>ton/år</span></p>
</div>
<div class="year-selector">
<button (click)="setYear(2030)" [class.active]="selectedYear() === 2030">2030</button>
<button (click)="setYear(2035)" [class.active]="selectedYear() === 2035">2035</button>
</div>
<dl class="tax-details">
<div class="tax-row">
<dt>Marginal afgiftssats</dt>
<dd>{{ taxResult().marginalRate | number: '1.0-0' : 'da' }} kr/ton</dd>
</div>
<div class="tax-row">
<dt>Bundfradrag</dt>
<dd>{{ taxResult().deductionPercent | number: '1.0-0' : 'da' }}%</dd>
</div>
<div class="tax-row highlight">
<dt>Effektiv sats</dt>
<dd>{{ taxResult().effectiveRate | number: '1.0-0' : 'da' }} kr/ton</dd>
</div>
</dl>
<div class="tax-box">
<h3>Årlig CO2e-afgift ({{ selectedYear() }})</h3>
<p>{{ formatDanish(taxResult().totalTax) }} <span>kr</span></p>
</div>
@if (totalEmission() > 0) {
<section class="comparison">
<h3>Sammenligning</h3>
<!-- ... timeline med 2030 vs 2035 ... -->
</section>
}
@if (breakdown().length > 0) {
<section class="breakdown">
@for (item of breakdown(); track item.animalType) {
<div class="breakdown-item">
<span>{{ item.animalType }} ({{ item.count }} stk)</span>
<span>{{ item.totalKgCO2e / 1000 | number: '1.1-1' : 'da' }} ton
({{ item.percentage | number: '1.0-0' : 'da' }}%)</span>
</div>
}
</section>
}
</div>
Læg mærke til mønstret: component'en opretter ikke sine egne beregninger. Den eksponerer bare servicens signals, så templaten kan bruge dem. Al logik bor ét sted (servicen), og component'en er bare en tynd skal.
I herd-input.html har du allerede en "Nulstil" knap. Forbind den til servicens reset() metode:
<!-- I herd-input.html: -->
<button class="reset-btn" (click)="onReset()">
<span class="material-symbols-outlined">restart_alt</span> Nulstil
</button>
// I HerdInput klassen:
onReset() {
this.emissionService.reset();
}
Men der er et problem! Når vi kalder reset(), opdateres signalet i servicen — men <input>-felterne i HerdCard viser stadig de gamle tal. Det skyldes at input-felterne ikke er bundet til servicens data. Vi skal forbinde dem.
1. Tilføj count input på HerdCard:
// calculator/herd-card/herd-card.ts
import { Component, input, output } from '@angular/core';
import { Card } from '../emission';
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 count = input<number>(0);
readonly countChanged = output<number>();
onCountChanged(count: number): void {
this.countChanged.emit(count);
}
get isActive() { return this.count() > 0; }
}
2. Bind [value] på input-elementet i herd-card.html:
<!-- I herd-card.html, på <input> elementet: -->
<input
#input
type="number"
[class.has-value]="isActive"
min="0"
placeholder="0"
(input)="onCountChanged(input.valueAsNumber)"
[value]="count()"
/>
[value]="count()" binder input-feltets værdi til det signal vi modtager fra parent. Når servicen nulstiller, opdateres signalet, og feltet viser 0.
3. Send herdCounts til kortene fra herd-input.html:
I HerdInput klassen skal du også eksponere herdCounts:
// I HerdInput klassen:
readonly herdCounts = this.emissionService.herdCounts;
Og i herd-input.html, send count til hvert kort:
<!-- herd-input.html -->
@for (herd of herdCards; track herd.key) {
<app-herd-card
[herd]="herd"
(countChanged)="onCountChanged(herd.key, $event)"
[count]="herdCounts()[herd.key]"
/>
}
Nu flyder data begge veje: brugerinput går op via countChanged, og servicens state (inkl. reset) flyder ned via [count]. Læs mere om signal inputs.
import { Injectable, signal, computed } from '@angular/core';
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;
}
export interface EmissionResult {
animalType: string;
count: number;
emissionFactor: number;
totalKgCO2e: number;
percentage: number;
}
export interface TaxResult {
year: 2030 | 2035;
marginalRate: number;
deductionPercent: number;
effectiveRate: number;
totalTax: number;
}
const TAX_RATES = {
2030: { marginal: 300, deduction: 0.6 },
2035: { marginal: 750, deduction: 0.6 },
} as const;
@Injectable({ providedIn: 'root' })
export class Emission {
readonly herdCards: Card[] = [
{ 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 },
];
readonly herdCounts = signal<HerdCounts>({
dairyCows: 0, beefCows: 0, calves: 0,
pigs: 0, sows: 0, chickens: 0,
});
readonly selectedYear = signal<2030 | 2035>(2030);
readonly totalEmissionsKg = computed(() => {
const counts = this.herdCounts();
return this.herdCards.reduce(
(sum, c) => sum + counts[c.key] * c.factor, 0
);
});
readonly totalEmission = computed(() => this.totalEmissionsKg() / 1000);
readonly emissionBreakdown = computed<EmissionResult[]>(() => {
const counts = this.herdCounts();
const totalKg = this.totalEmissionsKg();
return this.herdCards
.map(card => ({
animalType: card.label,
count: counts[card.key],
emissionFactor: card.factor,
totalKgCO2e: counts[card.key] * card.factor,
percentage: totalKg > 0
? (counts[card.key] * card.factor / totalKg) * 100 : 0,
}))
.filter(item => item.count > 0);
});
readonly taxResult = computed<TaxResult>(() => {
const year = this.selectedYear();
const tons = this.totalEmission();
const rates = TAX_RATES[year];
const effectiveRate = rates.marginal * (1 - rates.deduction);
return {
year,
marginalRate: rates.marginal,
deductionPercent: rates.deduction * 100,
effectiveRate,
totalTax: tons * effectiveRate,
};
});
readonly taxComparison = computed(() => {
const tons = this.totalEmission();
const tax2030 = tons * (TAX_RATES[2030].marginal * (1 - TAX_RATES[2030].deduction));
const tax2035 = tons * (TAX_RATES[2035].marginal * (1 - TAX_RATES[2035].deduction));
return {
tax2030, tax2035,
difference: tax2035 - tax2030,
percentIncrease: tax2030 > 0
? ((tax2035 - tax2030) / tax2030) * 100 : 0,
};
});
updateCount(key: keyof HerdCounts, count: number) {
this.herdCounts.update(prev => ({ ...prev, [key]: count }));
}
setYear(year: 2030 | 2035) {
this.selectedYear.set(year);
}
reset() {
this.herdCounts.set({
dairyCows: 0, beefCows: 0, calves: 0,
pigs: 0, sows: 0, chickens: 0,
});
}
}
// calculator/herd-input/herd-input.ts
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],
templateUrl: './herd-input.html',
styleUrl: './herd-input.css',
})
export class HerdInput {
private readonly emissionService = inject(Emission);
readonly herdCards = this.emissionService.herdCards;
readonly herdCounts = this.emissionService.herdCounts;
onCountChanged(key: keyof HerdCounts, count: number) {
this.emissionService.updateCount(key, count);
}
onReset() {
this.emissionService.reset();
}
}
// calculator/herd-card/herd-card.ts
import { Component, input, output } from '@angular/core';
import { Card } from '../emission';
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 count = input<number>(0);
readonly countChanged = output<number>();
onCountChanged(count: number): void {
this.countChanged.emit(count);
}
get isActive() { return this.count() > 0; }
}
// calculator/result-panel/result-panel.ts
import { DecimalPipe } from '@angular/common';
import { Component, inject } from '@angular/core';
import { Emission } from '../emission';
@Component({
selector: 'app-result-panel',
imports: [DecimalPipe],
templateUrl: './result-panel.html',
styleUrl: './result-panel.css',
})
export class ResultPanel {
private readonly emissionService = inject(Emission);
readonly selectedYear = this.emissionService.selectedYear;
readonly totalEmission = this.emissionService.totalEmission;
readonly taxResult = this.emissionService.taxResult;
readonly comparison = this.emissionService.taxComparison;
readonly breakdown = this.emissionService.emissionBreakdown;
setYear(year: 2030 | 2035) {
this.emissionService.setYear(year);
}
formatDanish(value: number): string {
return Math.round(value).toLocaleString('da-DK');
}
}
<!-- calculator/result-panel/result-panel.html -->
<div class="panel">
<div class="panel-header">
<span class="material-symbols-outlined">description</span>
<h2>Klimaregnskab</h2>
</div>
<div class="panel-content">
<div class="emission-hero">
<h3>Total CO2e-udledning</h3>
<p>{{ totalEmission() | number: '1.1-1' : 'da' }} <span>ton/år</span></p>
</div>
<div class="year-selector">
<button (click)="setYear(2030)" [class.active]="selectedYear() === 2030">2030</button>
<button (click)="setYear(2035)" [class.active]="selectedYear() === 2035">2035</button>
</div>
<dl class="tax-details">
<div class="tax-row">
<dt>Marginal afgiftssats</dt>
<dd>{{ taxResult().marginalRate | number: '1.0-0' : 'da' }} kr/ton</dd>
</div>
<div class="tax-row">
<dt>Bundfradrag</dt>
<dd>{{ taxResult().deductionPercent | number: '1.0-0' : 'da' }}%</dd>
</div>
<div class="tax-row highlight">
<dt>Effektiv sats</dt>
<dd>{{ taxResult().effectiveRate | number: '1.0-0' : 'da' }} kr/ton</dd>
</div>
</dl>
<div class="tax-box">
<h3>Årlig CO2e-afgift ({{ selectedYear() }})</h3>
<p>{{ formatDanish(taxResult().totalTax) }} <span>kr</span></p>
</div>
@if (totalEmission() > 0) {
<section class="comparison">
<h3>Sammenligning</h3>
<div class="timeline">
<div class="timeline-bar"></div>
<div class="timeline-points">
<div class="timeline-point">
<div class="timeline-dot"></div>
<span>2030</span>
<strong>{{ formatDanish(comparison().tax2030) }} kr</strong>
</div>
<span class="timeline-badge">
+{{ comparison().percentIncrease | number: '1.0-0' : 'da' }}% Stigning
</span>
<div class="timeline-point">
<div class="timeline-dot"></div>
<span>2035</span>
<strong>{{ formatDanish(comparison().tax2035) }} kr</strong>
</div>
</div>
</div>
<p class="comparison-pill">
+{{ formatDanish(comparison().difference) }} kr ekstra om året
</p>
</section>
}
@if (breakdown().length > 0) {
<section class="breakdown">
<h3>
<span class="material-symbols-outlined">pie_chart</span>
Fordeling af Udledning
</h3>
@for (item of breakdown(); track item.animalType) {
<div class="breakdown-item">
<div class="breakdown-label">
<span>{{ item.animalType }} ({{ item.count }} stk)</span>
<span>{{ item.totalKgCO2e / 1000 | number: '1.1-1' : 'da' }} ton ({{ item.percentage | number: '1.0-0' : 'da' }}%)</span>
</div>
<div class="progress-track">
<div class="progress-fill" [style.width.%]="item.percentage"></div>
</div>
</div>
}
</section>
}
<button class="save-btn">
<span class="material-symbols-outlined">save_alt</span>
Gem og Del Resultat
</button>
</div>
</div>
// calculator/calculator.ts
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],
template: `
<div class="layout">
<app-herd-input />
<app-result-panel />
</div>
`,
styles: `
.layout {
max-width: 80rem;
margin: 0 auto;
padding: 2.5rem 1rem;
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
align-items: start;
}
@media (min-width: 1024px) {
.layout { grid-template-columns: 7fr 5fr; }
}
`,
})
export class Calculator {}