Advent of JavaScript, Day 7

Advent of JS Homepage

Challenge #7 is creating a Tip Calculator that updates a dollar amount:

With the project files downloaded and codesandbox‘d into a live CodeSandbox, I’m ready to get going!


User Requirements

calculate tip based on tip percentage, bill amount, and number of people

When reviewing the included index.html, it looks like the data exists for wiring:

  • Tip – <span id="tip-amount">
  • Bill Amount – <span id="total-per-person">
  • Number of People – <input id="number-of-people">
  • Tip % – <input name="tip">

I could normally get a value via form[name], but since tip isn’t wrapped in a <form> this won’t work.

Instead, I can use CSS:checked property to find the selected tip amount:

document.querySelector('input[name=tip]:checked').value

Wiring it Up

I could use Vanilla JS and listen to events on each input, but I’d like to learn how Vue handles Radio inputs.

  1. Add Vue, again

    <script src="https://unpkg.com/vue@next"></script>
  2. Scaffold my TipCalculator:

    const TipCalculator = {
      data() {
        return {}
      },
    }
    
    Vue.createApp(TipCalculator).mount(document.querySelector('.wrapper'))

    I can do this from memory now 🤩

  3. Mapping each <input> to a v-model:

    https://v3.vuejs.org/guide/forms.html#text

    Apparently this will automatically map it to the data() object.

    I did change the values for tip from:

    <input type="radio" name="tip" value="15%" />

    to:

    <input type="radio" name="tip" value="0.05" v-model="tipPercentage" />
  4. Creating computed properties for tipAmount and totalPerPerson:

    JavaScript is notorious for erroneous floating-point arithmetic.

    The easiest way I’ve found to deal with currency is to simply multiply by 100 so that we’re dealing with whole numbers.

    Then, when dividing, use Math.ceil so that we don’t underpay our bill (or tip!).

    (number / 100).toFixed(2) will round back to two decimal places.

Finishing Up

This ended up being a very small app, but with some “gotchas”:

const roundUp = (number) => {
  return Number((Math.ceil(number * 100) / 100).toFixed(2))
}

const TipCalculator = {
  data() {
    return {
      billAmount: '102.02',
      numberOfPeople: '3',
      tipPercentage: '0.15',
    }
  },

  computed: {
    tipAmount() {
      return roundUp(Number(this.billAmount) * Number(this.tipPercentage))
    },
    totalPerPerson() {
      return roundUp(
        (Number(this.billAmount) + this.tipAmount) / Number(this.numberOfPeople)
      )
    },
  },
}

See all the casting to Number? If I RTFM, I would’ve seen v-model.number would cast it automatically:

https://v3.vuejs.org/guide/forms.html#number

With that updated, I do far less casting:

const roundUp = (number) => {
  return Number((Math.ceil(number * 100) / 100).toFixed(2))
}

const TipCalculator = {
  data() {
    return {
      billAmount: 102.02,
      numberOfPeople: 3,
      tipPercentage: 0.15,
    }
  },

  computed: {
    tipAmount() {
      return roundUp(this.billAmount * this.tipPercentage)
    },
    totalPerPerson() {
      return roundUp((this.billAmount + this.tipAmount) / this.numberOfPeople)
    },
  },
}