Loan Calculator
blockfinanceInteractive mortgage and commercial loan amortization schedule calculator with dynamic sliders, loan type presets, extra payment impact simulation, principal vs interest visual ratio bar, and year-by-year amortization breakdown table.
Installation
$pnpm dlx uipkge-ng@latest add loan-calculator$npx uipkge-ng@latest add loan-calculator$yarn dlx uipkge-ng@latest add loan-calculator$bunx uipkge-ng@latest add loan-calculatorManifest:
https://uipkge.dev/r/angular/loan-calculator.jsonInstalls to:src/app/components/blocks/loan-calculator/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
LoanPresetinterface LoanPreset {
id: LoanType
name: string
amount: number
rate: number
termYears: number
extraPayment: number
description: string
}MonthRecordinterface MonthRecord {
month: number
principalPaid: number
interestPaid: number
extraPaid: number
totalPaid: number
remainingBalance: number
}YearRecordinterface YearRecord {
year: number
principalPaid: number
interestPaid: number
extraPaid: number
totalPaid: number
remainingBalance: number
}AmortizationResultinterface AmortizationResult {
monthlySchedule: MonthRecord[]
yearlySchedule: YearRecord[]
actualMonths: number
actualYears: string
monthsSaved: number
yearsSaved: string
totalInterestPaid: number
totalLoanCost: number
totalExtraPaid: number
interestSavings: number
}Files installed (2)
src/app/components/blocks/loan-calculator/loan-calculator.component.ts32.2 kB
// Interactive mortgage and commercial loan amortization schedule calculator with dynamic // sliders, loan type presets, extra payment impact simulation, principal vs interest visual // ratio bar, and year-by-year amortization breakdown table. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { ArrowRight, Building2, Calculator, Calendar, Car, CheckCircle2, DollarSign, FileSpreadsheet, Home, LucideAngularModule, PiggyBank, Wallet, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiSliderComponent } from '@/ui/slider/slider.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableFooterComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' import { UiTabsComponent, UiTabsListComponent, UiTabsTriggerComponent } from '@/ui/tabs/tabs.component' export type LoanType = 'mortgage-30' | 'commercial-10' | 'auto-5' | 'personal-3' export interface LoanPreset { id: LoanType name: string amount: number rate: number termYears: number extraPayment: number description: string } export const LOAN_PRESETS: Record<LoanType, LoanPreset> = { 'mortgage-30': { id: 'mortgage-30', name: 'Mortgage 30Y', amount: 450_000, rate: 6.5, termYears: 30, extraPayment: 200, description: '30-year fixed home mortgage', }, 'commercial-10': { id: 'commercial-10', name: 'Commercial 10Y', amount: 1_200_000, rate: 7.25, termYears: 10, extraPayment: 500, description: '10-year commercial real estate loan', }, 'auto-5': { id: 'auto-5', name: 'Auto Loan 5Y', amount: 45_000, rate: 5.9, termYears: 5, extraPayment: 50, description: '60-month standard vehicle loan', }, 'personal-3': { id: 'personal-3', name: 'Personal Loan', amount: 25_000, rate: 9.5, termYears: 3, extraPayment: 25, description: '36-month fixed unsecured personal loan', }, } export const LOAN_AMOUNT_MARKS: Record<number, string> = { 100000: '$100K', 500000: '$500K', 1000000: '$1M', 2000000: '$2M', } export const INTEREST_RATE_MARKS: Record<number, string> = { 1: '1%', 5: '5%', 10: '10%', 15: '15%', } export const LOAN_TERM_MARKS: Record<number, string> = { 5: '5Y', 10: '10Y', 15: '15Y', 20: '20Y', 30: '30Y', } export const EXTRA_PAYMENT_MARKS: Record<number, string> = { 0: '$0', 500: '$500', 1000: '$1K', 2000: '$2K', } export const TERM_PRESETS = [5, 10, 15, 20, 30] export interface MonthRecord { month: number principalPaid: number interestPaid: number extraPaid: number totalPaid: number remainingBalance: number } export interface YearRecord { year: number principalPaid: number interestPaid: number extraPaid: number totalPaid: number remainingBalance: number } export function fmtCurrency(val: number): string { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2, }).format(val || 0) } export function fmtCurrencyWhole(val: number): string { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0, maximumFractionDigits: 0, }).format(val || 0) } export function baseMonthlyPaymentFor(principal: number, annualRate: number, termYears: number): number { const totalMonths = termYears * 12 if (principal <= 0 || totalMonths <= 0) return 0 if (annualRate <= 0) return principal / totalMonths const monthlyRate = annualRate / 100 / 12 const factor = Math.pow(1 + monthlyRate, totalMonths) if (!isFinite(factor) || factor <= 1) return principal / totalMonths return (principal * (monthlyRate * factor)) / (factor - 1) } export interface AmortizationResult { monthlySchedule: MonthRecord[] yearlySchedule: YearRecord[] actualMonths: number actualYears: string monthsSaved: number yearsSaved: string totalInterestPaid: number totalLoanCost: number totalExtraPaid: number interestSavings: number } export function amortize( loanAmount: number, interestRate: number, loanTermYears: number, extraMonthlyPayment: number, basePayment: number, totalInterestBase: number, ): AmortizationResult { const principal = Math.max(0, loanAmount) const annualRate = Math.max(0, interestRate) const termYears = Math.max(1, loanTermYears) const extra = Math.max(0, extraMonthlyPayment) const monthlyRate = annualRate / 100 / 12 const standardMonths = termYears * 12 let balance = principal const monthlySchedule: MonthRecord[] = [] let totalInterest = 0 let totalPaid = 0 let totalExtra = 0 let month = 0 const maxMonths = 1200 while (balance > 0.005 && month < maxMonths) { month++ const interestForMonth = annualRate > 0 ? balance * monthlyRate : 0 let scheduledTotal = basePayment + extra if (balance + interestForMonth < scheduledTotal) scheduledTotal = balance + interestForMonth const principalForMonth = Math.min(balance, Math.max(0, scheduledTotal - interestForMonth)) const extraForMonth = Math.max(0, scheduledTotal - basePayment) balance = Math.max(0, balance - principalForMonth) totalInterest += interestForMonth totalPaid += scheduledTotal totalExtra += extraForMonth monthlySchedule.push({ month, principalPaid: principalForMonth, interestPaid: interestForMonth, extraPaid: extraForMonth, totalPaid: scheduledTotal, remainingBalance: balance, }) } const yearlySchedule: YearRecord[] = [] const totalYearsActual = Math.ceil(monthlySchedule.length / 12) for (let y = 1; y <= totalYearsActual; y++) { const yearMonths = monthlySchedule.slice((y - 1) * 12, Math.min(y * 12, monthlySchedule.length)) if (yearMonths.length === 0) continue yearlySchedule.push({ year: y, principalPaid: yearMonths.reduce((s, m) => s + m.principalPaid, 0), interestPaid: yearMonths.reduce((s, m) => s + m.interestPaid, 0), extraPaid: yearMonths.reduce((s, m) => s + m.extraPaid, 0), totalPaid: yearMonths.reduce((s, m) => s + m.totalPaid, 0), remainingBalance: yearMonths[yearMonths.length - 1]?.remainingBalance ?? 0, }) } const monthsSaved = Math.max(0, standardMonths - monthlySchedule.length) return { monthlySchedule, yearlySchedule, actualMonths: monthlySchedule.length, actualYears: (monthlySchedule.length / 12).toFixed(1), monthsSaved, yearsSaved: (monthsSaved / 12).toFixed(1), totalInterestPaid: totalInterest, totalLoanCost: totalPaid, totalExtraPaid: totalExtra, interestSavings: Math.max(0, totalInterestBase - totalInterest), } } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-loan-calculator, [ui-loan-calculator]', standalone: true, host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, UiInputComponent, UiSeparatorComponent, UiSliderComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableFooterComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, UiTabsComponent, UiTabsListComponent, UiTabsTriggerComponent, ], template: ` <div data-slot="loan-calculator" [class]="rootClass"> <!-- Header --> <div class="flex flex-col items-center gap-4 text-center"> <span ui-badge variant="outline" class="gap-1.5 px-3 py-1 text-xs font-medium tracking-wider uppercase"> <lucide-icon [img]="Calculator" class="text-primary size-3.5" /> Loan Amortization Engine </span> <div class="space-y-2"> <h2 class="text-foreground text-2xl font-bold tracking-tight sm:text-3xl lg:text-4xl"> Loan & Mortgage Amortization Calculator </h2> <p class="text-muted-foreground mx-auto max-w-2xl text-sm sm:text-base"> Estimate monthly payments, interest vs principal breakdown, and payoff timelines with extra payments. </p> </div> <div class="mt-2 flex w-full justify-center"> <ui-tabs [value]="selectedLoanType()" (valueChange)="handleTabChange($event)" class="w-full sm:w-auto"> <ui-tabs-list variant="segmented" class="grid w-full grid-cols-2 sm:flex sm:w-auto"> <button ui-tabs-trigger value="mortgage-30" class="gap-1.5"> <lucide-icon [img]="Home" class="size-3.5" /> Mortgage 30Y </button> <button ui-tabs-trigger value="commercial-10" class="gap-1.5"> <lucide-icon [img]="Building2" class="size-3.5" /> Commercial 10Y </button> <button ui-tabs-trigger value="auto-5" class="gap-1.5"> <lucide-icon [img]="Car" class="size-3.5" /> Auto Loan 5Y </button> <button ui-tabs-trigger value="personal-3" class="gap-1.5"> <lucide-icon [img]="Wallet" class="size-3.5" /> Personal Loan </button> </ui-tabs-list> </ui-tabs> </div> </div> <!-- 2-Column Calculator Layout --> <div class="grid grid-cols-1 gap-8 lg:grid-cols-12 lg:items-start"> <div class="space-y-6 lg:col-span-7"> <div ui-card> <div ui-card-header> <h3 ui-card-title class="flex items-center gap-2"> <lucide-icon [img]="DollarSign" class="text-primary size-5" /> Loan Parameters </h3> <p ui-card-description> Customize loan principal, interest rate, term duration, and extra monthly contributions. </p> </div> <div ui-card-content class="space-y-8"> <div class="space-y-4"> <div class="flex items-center justify-between gap-4"> <div> <span class="text-foreground text-sm font-medium">Loan Amount (Principal)</span> <p class="text-muted-foreground text-xs">Total borrowed loan amount.</p> </div> <div class="w-36 sm:w-44"> <ui-input type="number" [value]="loanAmount()" (valueChange)="loanAmount.set(Number($event) || 0)" placeholder="$450,000" class="text-xs" /> </div> </div> <div class="pb-6"> <ui-slider [value]="[loanAmount()]" [min]="10000" [max]="2000000" [step]="5000" [marks]="amountMarks" (valueChange)="loanAmount.set($event[0] ?? loanAmount())" /> </div> <div class="flex flex-wrap items-center gap-1.5 pt-1"> <span class="text-muted-foreground text-xs">Quick select:</span> @for (amt of [150000, 300000, 450000, 750000, 1200000]; track amt) { <button ui-button variant="outline" size="sm" class="h-6 px-2 text-xs" (click)="loanAmount.set(amt)" > {{ amt >= 1000000 ? (amt / 1000000).toFixed(1) + 'M' : amt / 1000 + 'K' }} </button> } </div> </div> <div ui-separator></div> <div class="space-y-4"> <div class="flex items-center justify-between gap-4"> <div> <span class="text-foreground text-sm font-medium">Annual Interest Rate (APR)</span> <p class="text-muted-foreground text-xs">Fixed annual percentage rate.</p> </div> <div class="w-32 sm:w-36"> <ui-input type="number" [value]="interestRate()" (valueChange)="interestRate.set(Number($event) || 0)" placeholder="6.5%" class="text-xs" /> </div> </div> <div class="pb-6"> <ui-slider [value]="[interestRate()]" [min]="1" [max]="15" [step]="0.1" [marks]="rateMarks" (valueChange)="interestRate.set($event[0] ?? interestRate())" /> </div> <div class="flex flex-wrap items-center gap-1.5 pt-1"> <span class="text-muted-foreground text-xs">Market benchmarks:</span> @for (rate of [4.5, 5.5, 6.5, 7.5, 9.5]; track rate) { <button ui-button variant="outline" size="sm" class="h-6 px-2 text-xs" (click)="interestRate.set(rate)" > {{ rate }}% </button> } </div> </div> <div ui-separator></div> <div class="space-y-4"> <div class="flex items-center justify-between gap-4"> <div> <span class="text-foreground text-sm font-medium">Loan Term</span> <p class="text-muted-foreground text-xs">Amortization period in years.</p> </div> <div class="flex items-center gap-1.5"> <span ui-badge variant="secondary" class="font-mono text-xs font-semibold"> {{ loanTermYears() }} Years ({{ loanTermYears() * 12 }} mo) </span> </div> </div> <div class="pb-6"> <ui-slider [value]="[loanTermYears()]" [min]="1" [max]="30" [step]="1" [marks]="termMarks" (valueChange)="loanTermYears.set($event[0] ?? loanTermYears())" /> </div> <div class="flex flex-wrap items-center gap-1.5 pt-1"> <span class="text-muted-foreground text-xs">Standard terms:</span> @for (term of termPresets; track term) { <button ui-button [variant]="loanTermYears() === term ? 'default' : 'outline'" size="sm" class="h-6 px-2 text-xs" (click)="loanTermYears.set(term)" > {{ term }} Yrs </button> } </div> </div> <div ui-separator></div> <div class="space-y-4"> <div class="flex flex-wrap items-center justify-between gap-4"> <div> <div class="flex items-center gap-2"> <span class="text-foreground text-sm font-medium">Extra Monthly Payment</span> <span ui-badge variant="outline" class="border-success/20 bg-success/10 text-success text-xs font-normal" > Accelerates Payoff </span> </div> <p class="text-muted-foreground text-xs">Directly reduces principal balance each month.</p> </div> <div class="w-36 sm:w-44"> <ui-input type="number" [value]="extraMonthlyPayment()" (valueChange)="extraMonthlyPayment.set(Number($event) || 0)" placeholder="$200/mo" class="text-xs" /> </div> </div> <div class="pb-6"> <ui-slider [value]="[extraMonthlyPayment()]" [min]="0" [max]="2000" [step]="25" [marks]="extraMarks" (valueChange)="extraMonthlyPayment.set($event[0] ?? extraMonthlyPayment())" /> </div> <div class="flex flex-wrap items-center gap-1.5 pt-1"> <span class="text-muted-foreground text-xs">Quick extra:</span> @for (extra of [0, 100, 200, 500, 1000]; track extra) { <button ui-button variant="outline" size="sm" class="h-6 px-2 text-xs" (click)="extraMonthlyPayment.set(extra)" > +${{ extra }}/mo </button> } </div> </div> </div> </div> </div> <!-- Right Results Card --> <div class="lg:sticky lg:top-8 lg:col-span-5"> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-4"> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> Estimated Monthly Payment </span> <span ui-badge variant="secondary" class="bg-primary/10 text-primary border-primary/20 text-xs font-semibold" > Amortized </span> </div> <div class="mt-4"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground text-4xl font-bold tracking-tight tabular-nums sm:text-5xl"> {{ fmt(baseMonthlyPayment()) }} </span> <span class="text-muted-foreground text-sm font-normal"> / month</span> </div> @if (extraMonthlyPayment() > 0) { <p class="text-muted-foreground mt-2 text-xs"> Total monthly cash outlay: <span class="text-foreground font-semibold tabular-nums"> {{ fmt(baseMonthlyPayment() + extraMonthlyPayment()) }}/mo </span> ({{ fmt(baseMonthlyPayment()) }} base + {{ fmt(extraMonthlyPayment()) }} extra) </p> } @else { <p class="text-muted-foreground mt-2 text-xs"> Standard fixed payment based on {{ interestRate() }}% APR over {{ loanTermYears() }} years. </p> } </div> </div> <div ui-card-content class="space-y-5"> <div ui-separator></div> @if (extraMonthlyPayment() > 0 && amortization().interestSavings > 0) { <div class="border-success/20 bg-success/10 text-success rounded-lg border p-3.5 text-xs"> <div class="flex items-start gap-2.5"> <lucide-icon [img]="PiggyBank" class="mt-0.5 size-4 shrink-0" /> <div class="space-y-1"> <p class="text-sm leading-snug font-semibold"> Saves {{ fmtWhole(amortization().interestSavings) }} and cuts {{ amortization().yearsSaved }} years off payoff! </p> <p class="text-success/80 leading-normal"> You'll be debt-free in {{ amortization().actualYears }} years ({{ amortization().actualMonths }} payments) instead of {{ loanTermYears() }} years. </p> </div> </div> </div> } <div class="space-y-2.5"> <div class="flex items-center justify-between text-sm"> <span class="text-muted-foreground">Total Loan Principal</span> <span class="font-medium tabular-nums">{{ fmt(loanAmount()) }}</span> </div> <div class="flex items-center justify-between text-sm"> <span class="text-muted-foreground">Total Interest Paid</span> <span class="text-warning font-medium tabular-nums">{{ fmt(amortization().totalInterestPaid) }}</span> </div> <div class="flex items-center justify-between text-sm"> <span class="text-muted-foreground">Total Loan Cost</span> <span class="text-foreground font-semibold tabular-nums">{{ fmt(amortization().totalLoanCost) }}</span> </div> <div class="flex items-center justify-between text-sm"> <span class="text-muted-foreground">Effective Payoff Timeline</span> <span class="font-medium tabular-nums"> {{ amortization().actualYears }} Years ({{ amortization().actualMonths }} mo) </span> </div> </div> <div ui-separator></div> <div class="space-y-2.5"> <div class="flex items-center justify-between text-xs font-medium"> <span class="text-foreground">Principal vs. Total Interest</span> <span class="text-muted-foreground tabular-nums" >{{ principalPercent() }}% / {{ interestPercent() }}%</span > </div> <div class="bg-muted relative flex h-3 w-full overflow-hidden rounded-full shadow-inner"> <div class="bg-primary h-full transition-[width] duration-300" [style.width.%]="principalPercent()" ></div> <div class="bg-warning h-full transition-[width] duration-300" [style.width.%]="interestPercent()" ></div> </div> <div class="text-muted-foreground flex flex-col gap-1.5 text-xs sm:flex-row sm:items-center sm:justify-between" > <div class="flex items-center gap-1.5"> <span class="bg-primary size-2 shrink-0 rounded-full"></span> <span>Principal: {{ fmtWhole(loanAmount()) }} ({{ principalPercent() }}%)</span> </div> <div class="flex items-center gap-1.5"> <span class="bg-warning size-2 shrink-0 rounded-full"></span> <span>Interest: {{ fmtWhole(amortization().totalInterestPaid) }} ({{ interestPercent() }}%)</span> </div> </div> </div> <div ui-separator></div> <ul class="text-muted-foreground space-y-2 text-xs"> <li class="flex items-center gap-2"> <lucide-icon [img]="CheckCircle2" class="text-primary size-3.5 shrink-0" /> <span>Real-time compound interest calculations</span> </li> <li class="flex items-center gap-2"> <lucide-icon [img]="CheckCircle2" class="text-primary size-3.5 shrink-0" /> <span>Principal reduction extra payment model</span> </li> <li class="flex items-center gap-2"> <lucide-icon [img]="CheckCircle2" class="text-primary size-3.5 shrink-0" /> <span>Standard fixed-rate banking amortization schedule</span> </li> </ul> </div> <div ui-card-footer class="flex flex-col gap-2.5 pt-2"> <button ui-button class="w-full gap-2 font-semibold shadow-xs" size="lg"> Apply for Pre-Approval <lucide-icon [img]="ArrowRight" class="size-4" /> </button> <button ui-button aria-label="Download attachment" variant="outline" class="w-full gap-2" size="default"> <lucide-icon [img]="FileSpreadsheet" class="size-4" /> Download Schedule (CSV) </button> </div> </div> </div> </div> <!-- Amortization Schedule Table --> <div ui-card class="mt-8"> <div ui-card-header class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div> <h3 ui-card-title class="flex items-center gap-2"> <lucide-icon [img]="Calendar" class="text-primary size-5" /> Amortization Schedule </h3> <p ui-card-description> Annual breakdown of principal reduction, interest paid, extra contributions, and ending balance. </p> </div> <div class="flex items-center gap-2"> <button ui-button variant="outline" size="sm" class="text-xs" (click)="showAllYears.set(!showAllYears())"> {{ showAllYears() ? 'Show First 5 Years' : 'Show All ' + amortization().yearlySchedule.length + ' Years' }} </button> </div> </div> <div ui-card-content> <div class="overflow-x-auto"> <table ui-table density="cozy"> <thead ui-table-header> <tr ui-table-row> <th ui-table-head>Year</th> <th ui-table-head class="text-right">Principal Paid</th> <th ui-table-head class="text-right">Interest Paid</th> <th ui-table-head class="text-right">Extra Principal</th> <th ui-table-head class="text-right">Total Payment</th> <th ui-table-head class="text-right">Ending Balance</th> </tr> </thead> <tbody ui-table-body> @for (row of displayedSchedule(); track row.year) { <tr ui-table-row> <td ui-table-cell class="font-medium"> <div class="flex items-center gap-2"> <span>Year {{ row.year }}</span> @if (row.year === 1) { <span ui-badge variant="outline" class="text-xs font-normal">Initial</span> } </div> </td> <td ui-table-cell class="text-foreground text-right font-medium tabular-nums"> {{ fmt(row.principalPaid) }} </td> <td ui-table-cell class="text-warning text-right tabular-nums">{{ fmt(row.interestPaid) }}</td> <td ui-table-cell class="text-success text-right tabular-nums"> {{ row.extraPaid > 0 ? '+' + fmt(row.extraPaid) : '—' }} </td> <td ui-table-cell class="text-right font-semibold tabular-nums">{{ fmt(row.totalPaid) }}</td> <td ui-table-cell class="text-right font-semibold tabular-nums"> {{ fmt(row.remainingBalance) }} </td> </tr> } </tbody> <tfoot ui-table-footer> <tr ui-table-row> <td ui-table-cell class="font-bold"> Total ({{ displayedSchedule().length }} {{ displayedSchedule().length === 1 ? 'Year' : 'Years' }}) </td> <td ui-table-cell class="text-foreground text-right font-bold tabular-nums"> {{ fmt(displayedTotals().principal) }} </td> <td ui-table-cell class="text-warning text-right font-bold tabular-nums"> {{ fmt(displayedTotals().interest) }} </td> <td ui-table-cell class="text-success text-right font-bold tabular-nums"> {{ fmt(displayedTotals().extra) }} </td> <td ui-table-cell class="text-right font-bold tabular-nums">{{ fmt(displayedTotals().total) }}</td> <td ui-table-cell class="text-right font-bold tabular-nums"> {{ fmt(displayedSchedule()[displayedSchedule().length - 1]?.remainingBalance ?? 0) }} </td> </tr> </tfoot> </table> </div> </div> </div> </div> `, }) export class UiLoanCalculatorComponent { protected readonly ArrowRight = ArrowRight protected readonly Building2 = Building2 protected readonly Calculator = Calculator protected readonly Calendar = Calendar protected readonly Car = Car protected readonly CheckCircle2 = CheckCircle2 protected readonly DollarSign = DollarSign protected readonly FileSpreadsheet = FileSpreadsheet protected readonly Home = Home protected readonly PiggyBank = PiggyBank protected readonly Wallet = Wallet protected readonly amountMarks = LOAN_AMOUNT_MARKS protected readonly rateMarks = INTEREST_RATE_MARKS protected readonly termMarks = LOAN_TERM_MARKS protected readonly extraMarks = EXTRA_PAYMENT_MARKS protected readonly termPresets = TERM_PRESETS @Input('class') className?: string readonly selectedLoanType = signal<LoanType>('mortgage-30') readonly loanAmount = signal(450_000) readonly interestRate = signal(6.5) readonly loanTermYears = signal(30) readonly extraMonthlyPayment = signal(200) readonly showAllYears = signal(false) readonly baseMonthlyPayment = computed(() => baseMonthlyPaymentFor(this.loanAmount(), this.interestRate(), this.loanTermYears()), ) readonly totalCostBase = computed(() => this.baseMonthlyPayment() * this.loanTermYears() * 12) readonly totalInterestBase = computed(() => Math.max(0, this.totalCostBase() - this.loanAmount())) readonly amortization = computed(() => amortize( this.loanAmount(), this.interestRate(), this.loanTermYears(), this.extraMonthlyPayment(), this.baseMonthlyPayment(), this.totalInterestBase(), ), ) readonly principalPercent = computed(() => { const total = this.amortization().totalLoanCost if (total <= 0) return 100 return Math.min(100, Math.max(0, Math.round((this.loanAmount() / total) * 100))) }) readonly interestPercent = computed(() => Math.max(0, 100 - this.principalPercent())) readonly displayedSchedule = computed(() => this.showAllYears() ? this.amortization().yearlySchedule : this.amortization().yearlySchedule.slice(0, 5), ) readonly displayedTotals = computed(() => { const list = this.displayedSchedule() return { principal: list.reduce((s, r) => s + r.principalPaid, 0), interest: list.reduce((s, r) => s + r.interestPaid, 0), extra: list.reduce((s, r) => s + r.extraPaid, 0), total: list.reduce((s, r) => s + r.totalPaid, 0), } }) get rootClass(): string { return cn('mx-auto w-full max-w-6xl space-y-10 p-4 sm:p-6 lg:p-8', this.className) } fmt(v: number): string { return fmtCurrency(v) } fmtWhole(v: number): string { return fmtCurrencyWhole(v) } handleTabChange(value: string): void { const type = value as LoanType this.selectedLoanType.set(type) const preset = LOAN_PRESETS[type] if (preset) { this.loanAmount.set(preset.amount) this.interestRate.set(preset.rate) this.loanTermYears.set(preset.termYears) this.extraMonthlyPayment.set(preset.extraPayment) } } }src/app/components/blocks/loan-calculator/index.ts0.4 kB
export { UiLoanCalculatorComponent, LOAN_PRESETS, LOAN_AMOUNT_MARKS, INTEREST_RATE_MARKS, LOAN_TERM_MARKS, EXTRA_PAYMENT_MARKS, TERM_PRESETS, fmtCurrency, fmtCurrencyWhole, baseMonthlyPaymentFor, amortize, type LoanType, type LoanPreset, type MonthRecord, type YearRecord, type AmortizationResult, } from './loan-calculator.component'
Raw manifest:https://uipkge.dev/r/angular/loan-calculator.json