Files
sure/app/javascript/controllers/goal_contribution_preview_controller.js
Guillem Arias 8a414e4777 fix(goal_contributions/preview): rename templateNonZero -> templateNonzero so Stimulus matches the data attribute
Stimulus converts the JS value name templateNonZero to a kebab-cased
attribute by splitting on each capital letter, giving
data-...-template-non-zero-value. Rails' dataset helper converts the
Ruby key :goal_contribution_preview_template_nonzero_value to
data-...-template-nonzero-value (no hyphen between non and zero).

Result: the Stimulus controller resolved templateNonzeroValue to ""
and the preview pane went blank as soon as the user typed an amount.

Renaming the JS value to templateNonzero closes the conversion gap.
Verified live via Playwright: at $500 the preview reads "Will bring
you to 28% saved ($13,750 of $50,000)."; at $40,000 it flips to
"Will reach your $50,000 target."
2026-05-11 20:34:11 +02:00

67 lines
2.0 KiB
JavaScript

import { Controller } from "@hotwired/stimulus";
// Live impact preview for the add-contribution modal. Reads current
// balance + target amount from values and updates a preview sentence
// each keystroke. Template strings come from ERB so the wording stays
// localized.
export default class extends Controller {
static targets = ["amountInput", "preview"];
static values = {
currentBalance: Number,
targetAmount: Number,
currency: String,
templateZero: String,
templateNonzero: String,
templateReached: String,
};
connect() {
this.update();
}
update() {
if (!this.hasPreviewTarget) return;
const amount = this.#amountValue();
const newTotal = this.currentBalanceValue + amount;
const target = this.targetAmountValue;
const reached = newTotal >= target && target > 0;
const percent = target > 0 ? Math.min(100, Math.round((newTotal / target) * 100)) : 0;
let text;
if (reached) {
text = this.templateReachedValue.replace("{target}", this.#money(target));
} else if (amount === 0) {
text = this.templateZeroValue
.replaceAll("{percent}", percent.toString())
.replaceAll("{current}", this.#money(this.currentBalanceValue))
.replaceAll("{target}", this.#money(target));
} else {
text = this.templateNonzeroValue
.replaceAll("{percent}", percent.toString())
.replaceAll("{newTotal}", this.#money(newTotal))
.replaceAll("{target}", this.#money(target));
}
this.previewTarget.textContent = text;
}
#amountValue() {
if (!this.hasAmountInputTarget) return 0;
const parsed = Number.parseFloat(this.amountInputTarget.value);
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
}
#money(value) {
try {
return new Intl.NumberFormat(undefined, {
style: "currency",
currency: this.currencyValue || "USD",
maximumFractionDigits: 0,
}).format(value);
} catch {
return `${this.currencyValue || "$"}${Math.round(value).toLocaleString()}`;
}
}
}