One HTML file. One script tag. Live reactivity.
Built with wildflower.nano.min.js
<div data-component="tip-splitter">
<input type="number" data-model="bill" placeholder="0.00">
<button data-action="setTip" data-tip="15" data-bind-class="tipClass15">15%</button>
<!-- ...one preset per rate... -->
<button data-action="fewer">−</button>
<span data-bind="people"></span>
<button data-action="more">+</button>
<div data-render="tooFew">Add at least one person to split the bill.</div>
<div class="per" data-bind="perPerson"></div>
<span data-bind="tipTotal"></span>
<span data-bind="grandTotal"></span>
</div>
const money = n => '$' + (Number.isFinite(n) ? n : 0).toFixed(2);
wildflower.component('tip-splitter', {
state: {
bill: '',
tip: 15,
people: 2
},
computed: {
billNum() { return parseFloat(this.bill) || 0; },
tooFew() { return this.people < 1; },
tipTotalNum() { return this.billNum * this.tip / 100; },
grandTotalNum() { return this.billNum + this.tipTotalNum; },
perPersonNum() { return this.people > 0 ? this.grandTotalNum / this.people : 0; },
tipTotal() { return money(this.tipTotalNum); },
grandTotal() { return money(this.grandTotalNum); },
perPerson() { return money(this.perPersonNum); },
// Reactive highlight for the active preset button
tipClass10() { return this.tip === 10 ? 'on' : ''; },
tipClass15() { return this.tip === 15 ? 'on' : ''; },
tipClass18() { return this.tip === 18 ? 'on' : ''; },
tipClass20() { return this.tip === 20 ? 'on' : ''; }
},
setTip(event, el) {
this.tip = parseInt(el.dataset.tip, 10);
},
more() { this.people++; },
fewer() { if (this.people > 0) this.people--; }
});