LoanCalculator
A loan's amount, rate and term as sliders and boxes, with the monthly payment, total interest and total paid following as they move.
Demo
Loan calculator
% a year
years
Monthly payment
- Loan amount
- $25,000.00
- Total interest
- $4,349.20
- Payments
- 60
- Total paid
- $29,349.20
Home loan
% a year
years
Monthly payment
- Loan amount
- 320.000,00 €
- Total interest
- 363.291,35 €
- Payments
- 360
- Total paid
- 683.291,35 €
Move a slider: on_change gets the new payment.
Bike finance
% a year
months
Monthly payment
- Loan amount
- $4,800.00
- Total interest
- $0.00
- Payments
- 24
- Total paid
- $4,800.00
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/loan_calculator.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/loan_calculator.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/loan_calculator.qwe
component LoanCalculatorDemoLive() { state payment = 0m state total = 0m <div.demo-stack> <LoanCalculator id="loan-calculator-demo-home" name="home" label="Home loan" amount={320000} amount_least={50000} amount_most={1000000} amount_step={5000} rate={5.9} rate_most={12} term={30} term_least={5} term_most={35} currency="EUR" locale="de-DE" on_change={(q) => { payment = q.payment; total = q.total }}/> <p.demo-note id="loan-calculator-demo-note">{payment > 0 ? `on_change: ${payment} a month, ${total} in all.` : "Move a slider: on_change gets the new payment."}</p> </div>}view LoanCalculatorDemo() { <div.demo-stack> <LoanCalculator id="loan-calculator-demo-car" name="car" amount={25000} rate={6.5} term={5}/> <LoanCalculatorDemoLive/> <div.demo-narrow> <LoanCalculator id="loan-calculator-demo-months" name="bike" label="Bike finance" amount={4800} amount_least={500} amount_most={10000} amount_step={100} rate={0} term={24} term_unit="months" term_least={6} term_most={48}/> </div> </div>}Source
ui/loan_calculator.qwe
// LoanCalculator: how much a loan costs. The amount, the yearly rate and the term// each have a slider and a box to type in; the monthly payment (the annuity formula,// rounded to the cent), the total interest and the total paid follow as they move,// with a bar showing how much of what is paid is interest. Worked in decimals, from// a schedule whose last payment takes up the cents, so the totals are those a lender// would show (and AmortizationTable's for the same loan). The boxes are real// <input>s named `name` + "_amount", "_rate" and "_term", so a plain <form> sends them.//// <LoanCalculator amount={25000} rate={6.5} term={5}/>// <LoanCalculator amount={320000} amount_most={1000000} amount_step={5000} rate={5.9} term={30} currency="EUR" locale="de-DE"// on_change={(q) => { payment = q.payment }}/>// <LoanCalculator amount={18000} term={48} term_unit="months" term_least={12} term_most={84}/>//// amount, rate, term: where it starts (25,000; 6.5 % a year; 5). term_unit: "years"// (default) or "months". amount_least / amount_most / amount_step: the amount's// range and step (1,000 to 100,000 by 500); rate_least / rate_most / rate_step the// rate's (0 to 20 by 0.05); term_least / term_most the term's (1 to 30), in steps of// one. What is typed is kept to the range and the step when the box is left.// currency: ISO code ("USD"). locale: how amounts are written (the program's; en-US).// name: the prefix of the boxes' names and ids ("loan"). label: the heading, also// what screen readers call it ("Loan calculator"; null for none). on_change(q):// inside one of your components, given { amount, rate, term, months, payment,// interest, total } (decimals; term in term_unit) after each change. id, class.import ui/theme.qwe use ui_clslet loan_calculator_css = css`.ui-loan-calculator { container-type: inline-size; min-width: 0; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.45 var(--ui-font); }.ui-loan-calculator-grid { display: grid; gap: 0; }@container (min-width: 620px) { .ui-loan-calculator-grid { grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); } }.ui-loan-calculator-inputs { display: grid; gap: 22px; align-content: start; padding: 20px 22px 22px; }.ui-loan-calculator-title { margin: 0; font-size: 16px; font-weight: 650; }.ui-loan-calculator-field { display: grid; gap: 8px; }.ui-loan-calculator-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }.ui-loan-calculator-head label { font-size: 13px; font-weight: 500; }.ui-loan-calculator-box { display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: 0 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-bg); transition: border-color .15s, box-shadow .15s; }.ui-loan-calculator-box:hover { border-color: var(--ui-strong); }.ui-loan-calculator-box:focus-within { border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-loan-calculator-box input { width: 7.5em; min-width: 0; padding: 0; border: 0; outline: none; background: transparent; color: var(--ui-fg); font: 600 15px var(--ui-font); font-variant-numeric: tabular-nums; text-align: right; }.ui-loan-calculator-box input.ui-loan-calculator-short { width: 3.6em; }.ui-loan-calculator-unit { color: var(--ui-muted); font-size: 13px; }.ui-loan-calculator-range { --ui-loan-calculator-fill: 0%; appearance: none; display: block; width: 100%; height: 22px; margin: 0; background: transparent; cursor: pointer; }.ui-loan-calculator-range:focus { outline: none; }.ui-loan-calculator-range::-webkit-slider-runnable-track { height: 6px; border-radius: 999px; box-shadow: inset 0 0 0 1px var(--ui-border); background: linear-gradient(to right, var(--ui-accent) var(--ui-loan-calculator-fill), var(--ui-subtle) var(--ui-loan-calculator-fill)); }.ui-loan-calculator-range::-moz-range-track { height: 6px; border-radius: 999px; background: var(--ui-subtle); box-shadow: inset 0 0 0 1px var(--ui-border); }.ui-loan-calculator-range::-moz-range-progress { height: 6px; border-radius: 999px; background: var(--ui-accent); }.ui-loan-calculator-range::-webkit-slider-thumb { appearance: none; width: 18px; height: 18px; margin-top: -6px; border-radius: 50%; background: var(--ui-surface); border: 2px solid var(--ui-accent); box-shadow: var(--ui-shadow); }.ui-loan-calculator-range::-moz-range-thumb { width: 14px; height: 14px; border-radius: 50%; background: var(--ui-surface); border: 2px solid var(--ui-accent); }.ui-loan-calculator-range:focus-visible::-webkit-slider-thumb { box-shadow: var(--ui-focus); }.ui-loan-calculator-range:focus-visible::-moz-range-thumb { box-shadow: var(--ui-focus); }.ui-loan-calculator-limits { display: flex; justify-content: space-between; margin-top: -4px; color: var(--ui-muted); font-size: 12px; font-variant-numeric: tabular-nums; }.ui-loan-calculator-result { display: grid; gap: 14px; align-content: start; padding: 20px 22px 22px; border-top: 1px solid var(--ui-border); background: var(--ui-subtle); border-radius: 0 0 var(--ui-radius) var(--ui-radius); }@container (min-width: 620px) { .ui-loan-calculator-result { border-top: 0; border-left: 1px solid var(--ui-border); border-radius: 0 var(--ui-radius) var(--ui-radius) 0; } }.ui-loan-calculator-caption { margin: 0; color: var(--ui-muted); font-size: 13px; font-weight: 500; }.ui-loan-calculator-payment { display: block; margin-top: -10px; font-size: 34px; font-weight: 700; letter-spacing: -.015em; line-height: 1.2; font-variant-numeric: tabular-nums; }.ui-loan-calculator-per { color: var(--ui-muted); font-size: 14px; font-weight: 500; letter-spacing: 0; }.ui-loan-calculator-bar { display: flex; height: 10px; overflow: hidden; border-radius: 999px; background: var(--ui-warning); }.ui-loan-calculator-bar span { background: var(--ui-accent); }.ui-loan-calculator-key { display: flex; flex-wrap: wrap; gap: 4px 14px; margin: -6px 0 0; padding: 0; list-style: none; color: var(--ui-muted); font-size: 12px; }.ui-loan-calculator-key li { display: inline-flex; align-items: center; gap: 6px; }.ui-loan-calculator-key li::before { content: ""; width: 9px; height: 9px; border-radius: 2px; background: var(--ui-accent); }.ui-loan-calculator-key li + li::before { background: var(--ui-warning); }.ui-loan-calculator-totals { display: grid; grid-template-columns: 1fr auto; gap: 8px 0; margin: 0; }.ui-loan-calculator-totals dd { padding-left: 16px; }.ui-loan-calculator-totals dt { color: var(--ui-muted); }.ui-loan-calculator-totals dd { margin: 0; text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; }.ui-loan-calculator-totals .ui-loan-calculator-sum { padding-top: 8px; border-top: 1px solid var(--ui-border); color: var(--ui-fg); font-weight: 650; }`// x kept between least and most, on a step counted from least.fn loan_calculator_keep(x: dec, least: dec, most: dec, step: dec) -> dec { let snapped = step > 0 ? least + round((x - least) / step) * step : x snapped < least ? least : (snapped > most ? most : snapped)}// x to the power n, for a whole n, exactly (pow takes no decimals).fn loan_calculator_pow(x: dec, n: int) -> dec { let out = 1m let i = 0 while i < n { out = out * x i += 1 } out}// The monthly payment of an amount at a yearly rate in percent over a number of// months, rounded half up to the cent: amount × r / (1 − (1 + r)^−n), r a month's rate.fn loan_calculator_payment(amount: dec, rate: dec, months: int) -> dec { if months <= 0 { return amount } if rate <= 0 { return round(amount / months, 2, "ceiling") } let r = rate / 1200 let f = loan_calculator_pow(1m + r, months) round(amount * r * f / (f - 1m), 2, "half_up")}// { payment, interest, total } of the loan, from its schedule: each month's interest// rounded to the cent on what is still owed, and the last payment what is left.fn loan_calculator_totals(amount: dec, rate: dec, months: int) -> map { let pay = loan_calculator_payment(amount, rate, months) let r = rate / 1200 let owed = amount let interest = 0m let i = 1 while i <= months && owed > 0 { let part = round(owed * r, 2, "half_up") let principal = i == months || pay - part > owed ? owed : pay - part interest = interest + part owed = owed - principal i += 1 } { payment: pay, interest, total: amount + interest }}// An amount in the currency; whole: without the cents of a whole amount ("$25,000").fn loan_calculator_money(x: dec, currency: str, locale: str?, whole: bool = false) -> str { let m = money.of(x, currency, "half_up") if is_err(m) { return str(x) } let text = locale != null ? money.format(m, locale) : money.format(m) let units = money.minor_units(currency) !whole || units == 0 || x != floor(x) ? text : re.replace(text, `[.,]0{${units}}(\\D*)$`, "$1")}// Text from a box as a decimal (spaces, commas and the like dropped), or null.fn loan_calculator_read(text: str) -> dec? { let d = dec(re.replace(text, "[^0-9.]", "")) is_err(d) ? null : d}component LoanCalculator(amount = 25000, rate = 6.5, term: int = 5, term_unit: str = "years", amount_least = 1000, amount_most = 100000, amount_step = 500, rate_least = 0, rate_most = 20, rate_step = 0.05, term_least: int = 1, term_most: int = 30, currency: str = "USD", locale: str? = null, name: str = "loan", label: str? = "Loan calculator", on_change = null, id: str? = null, class: str? = null) { style global loan_calculator_css let a_lo = dec(amount_least) let a_hi = dec(amount_most) let a_step = dec(amount_step) let r_lo = dec(rate_least) let r_hi = dec(rate_most) let r_step = dec(rate_step) state a = loan_calculator_keep(dec(amount), dec(amount_least), dec(amount_most), dec(amount_step)) state r = loan_calculator_keep(dec(rate), dec(rate_least), dec(rate_most), dec(rate_step)) state t = int(loan_calculator_keep(dec(term), dec(term_least), dec(term_most), 1m)) state a_text = str(loan_calculator_keep(dec(amount), dec(amount_least), dec(amount_most), dec(amount_step))) state r_text = str(loan_calculator_keep(dec(rate), dec(rate_least), dec(rate_most), dec(rate_step))) state t_text = str(loan_calculator_keep(dec(term), dec(term_least), dec(term_most), 1m)) let years = term_unit != "months" let months = years ? t * 12 : t let q = loan_calculator_totals(a, r, months) let principal_share = q.total > 0 ? round(a * 100 / q.total, 1) : 100m let base = `loan-calculator-${name}` let unit_text = years ? (t == 1 ? "year" : "years") : (t == 1 ? "month" : "months") let fill = (v: dec, lo: dec, hi: dec) => hi > lo ? `${round((v - lo) * 100 / (hi - lo), 2)}%` : "0%" let changed = () => { if on_change != null { let m = years ? t * 12 : t on_change(merge({ amount: a, rate: r, term: t, months: m }, loan_calculator_totals(a, r, m))) } } let set_a = (x: dec?) => { let k = x == null ? a : loan_calculator_keep(x, a_lo, a_hi, a_step) a_text = str(k) if k != a { a = k; changed() } } let set_r = (x: dec?) => { let k = x == null ? r : loan_calculator_keep(x, r_lo, r_hi, r_step) r_text = str(k) if k != r { r = k; changed() } } let set_t = (x: dec?) => { let k = x == null ? t : int(loan_calculator_keep(round(x), dec(term_least), dec(term_most), 1m)) t_text = str(k) if k != t { t = k; changed() } } <section class={ui_cls(["ui-loan-calculator", class])} id={id} aria-label={label ?? "Loan calculator"}> <div.ui-loan-calculator-grid> <div.ui-loan-calculator-inputs> {label != null && <h3.ui-loan-calculator-title>{label}</h3>} <div.ui-loan-calculator-field> <div.ui-loan-calculator-head> <label for={`${base}-amount`}>Loan amount</label> <span.ui-loan-calculator-box> <input id={`${base}-amount`} name={`${name}_amount`} type="text" inputmode="decimal" autocomplete="off" value={a_text} @input={(e) => { a_text = e.value let x = loan_calculator_read(e.value) if x != null && x >= a_lo && x <= a_hi && x != a { a = x; changed() } }} @change={(e) => { set_a(loan_calculator_read(e.value)) }}/> <span.ui-loan-calculator-unit aria-hidden="true">{currency}</span> </span> </div> <input.ui-loan-calculator-range type="range" min={a_lo} max={a_hi} step={a_step} value={a} aria-label="Loan amount" aria-valuetext={loan_calculator_money(a, currency, locale, true)} style={`--ui-loan-calculator-fill: ${fill(a, a_lo, a_hi)}`} @input={(e) => { set_a(loan_calculator_read(e.value)) }}/> <div.ui-loan-calculator-limits aria-hidden="true"> <span>{loan_calculator_money(a_lo, currency, locale, true)}</span><span>{loan_calculator_money(a_hi, currency, locale, true)}</span> </div> </div> <div.ui-loan-calculator-field> <div.ui-loan-calculator-head> <label for={`${base}-rate`}>Interest rate</label> <span.ui-loan-calculator-box> <input.ui-loan-calculator-short id={`${base}-rate`} name={`${name}_rate`} type="text" inputmode="decimal" autocomplete="off" value={r_text} aria-describedby={`${base}-rate-unit`} @input={(e) => { r_text = e.value let x = loan_calculator_read(e.value) if x != null && x >= r_lo && x <= r_hi && x != r { r = x; changed() } }} @change={(e) => { set_r(loan_calculator_read(e.value)) }}/> <span.ui-loan-calculator-unit id={`${base}-rate-unit`}>% a year</span> </span> </div> <input.ui-loan-calculator-range type="range" min={r_lo} max={r_hi} step={r_step} value={r} aria-label="Interest rate" aria-valuetext={`${r}% a year`} style={`--ui-loan-calculator-fill: ${fill(r, r_lo, r_hi)}`} @input={(e) => { set_r(loan_calculator_read(e.value)) }}/> <div.ui-loan-calculator-limits aria-hidden="true"><span>{r_lo}%</span><span>{r_hi}%</span></div> </div> <div.ui-loan-calculator-field> <div.ui-loan-calculator-head> <label for={`${base}-term`}>Term</label> <span.ui-loan-calculator-box> <input.ui-loan-calculator-short id={`${base}-term`} name={`${name}_term`} type="text" inputmode="numeric" autocomplete="off" value={t_text} aria-describedby={`${base}-term-unit`} @input={(e) => { t_text = e.value let x = loan_calculator_read(e.value) if x != null && x == floor(x) && x >= dec(term_least) && x <= dec(term_most) && x != t { t = x; changed() } }} @change={(e) => { set_t(loan_calculator_read(e.value)) }}/> <span.ui-loan-calculator-unit id={`${base}-term-unit`}>{unit_text}</span> </span> </div> <input.ui-loan-calculator-range type="range" min={term_least} max={term_most} step="1" value={t} aria-label="Term" aria-valuetext={`${t} ${unit_text}`} style={`--ui-loan-calculator-fill: ${fill(dec(t), dec(term_least), dec(term_most))}`} @input={(e) => { set_t(loan_calculator_read(e.value)) }}/> <div.ui-loan-calculator-limits aria-hidden="true"> <span>{term_least} {years ? "yr" : "mo"}</span><span>{term_most} {years ? "yr" : "mo"}</span> </div> </div> </div> <div.ui-loan-calculator-result> <p.ui-loan-calculator-caption id={`${base}-pay-label`}>Monthly payment</p> <output.ui-loan-calculator-payment for={`${base}-amount ${base}-rate ${base}-term`} aria-labelledby={`${base}-pay-label`}> {loan_calculator_money(q.payment, currency, locale)}<span.ui-loan-calculator-per> / month</span> </output> <div.ui-loan-calculator-bar aria-hidden="true"><span style={`width: ${principal_share}%`}></span></div> <ul.ui-loan-calculator-key aria-hidden="true"> <li>Principal {principal_share}%</li> <li>Interest {100m - principal_share}%</li> </ul> <dl.ui-loan-calculator-totals> <dt>Loan amount</dt><dd data-key="amount">{loan_calculator_money(a, currency, locale)}</dd> <dt>Total interest</dt><dd data-key="interest">{loan_calculator_money(q.interest, currency, locale)}</dd> <dt>Payments</dt><dd data-key="months">{months}</dd> <dt.ui-loan-calculator-sum>Total paid</dt><dd.ui-loan-calculator-sum data-key="total">{loan_calculator_money(q.total, currency, locale)}</dd> </dl> </div> </div> </section>}Related
LoanEligibilityFour questions (income, debts, work, amount) and, as they are answered, whether a loan is likely, the amount within reach and why.7 views · 6 downloadsCurrencyConverterAn amount and two currencies with a swap button, the rate both ways and the converted amount, crossed through the base from the rates given.8 views · 11 downloadsPatronageRefundCalcWorks out a co-op member's dividend on shares and patronage refund on interest paid or purchases, to the total, as they type.7 views · 4 downloadsRemittanceQuoteSending money abroad: you send, the fee, the rate against the mid-market rate, what they get and when it arrives, worked out as the amount changes.8 views · 5 downloadsAccountPickerChoose the account to pay from as radio cards with each one's masked number and available balance, those short of the amount marked.7 views · 7 downloadsAmortizationTableA loan's payment schedule worked out from its amount, rate and term: each month's principal, interest and balance, grouped by year with the year's totals.7 views · 7 downloads
Thread
Thread 0 posts
On LoanCalculator: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.