Now that we have a way to create categories, it’s time for handling bills.

We want to:

  1. have a way to enter a bill
  2. list all the bills

Let’s start with the first one. We’ll list bills in the next lesson.

Bills are stored in the App component, in its state bills property (an array). We already have the property in place.

Some design decisions I made:

  • for each bill, we’ll store an object into this array, with a date, a category name, and the amount.
  • the category is a string that represents the category name, not a reference to the category.
  • for convenience, all bills are expressed in $, but you can, of course, choose your own currency symbol.

Here’s an example bill object:

{
  date: '2018-01-30T15:08:26.118Z',
  amount: '220',
  category: 'Electricity'
}

I’m going to create an AddBill component, which will be very similar to the AddCategory component, except this time we have 2 more fields. One is a date picker, and another is a select that shows the categories list.

Let’s do it! First, let’s replicate the thing we have in the AddCategory component:

<template>
  <div>
    <h1>Enter a new bill</h1>
    <p></p>
    <div>
      <input placeholder="Set amount" v-model="amount" />
      <button @click="handleClick">Add</button>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'AddBill',
    data: function () {
      return {
        amount: 0,
      }
    },
    methods: {
      handleClick: function () {
        //we'll do it later
      },
    },
  }
</script>

Now, as mentioned we need a select that shows the categories. Those categories must be passed by the parent component App since AddBill does not have any notion of what categories are and what they are used for.

We pass the categories array as a prop, and we set the first as the default category:

<script>
  export default {
    name: 'AddBill',
    props: ['categories'],
    data: function () {
      return {
        category: this.categories[0],
        amount: 0,
      }
    },
  }
</script>

Now we can add the select element into our form:

<select v-model="category">
  <option v-for="category in categories" :value="category" :key="category">
    {{ category }}
  </option>
</select>

We create an option for each category that’s passed as a prop by looping using v-for, and we set the selected option to match the category property in the component state, using v-model.

You can read more on how to use selects in the official docs.

Now it’s the turn of the last form element: the date picker.

Datepickers can be hard, and so as a good lazy developer I searched “vue datepicker” on Google, and I found this.

All we need to do is to add vuejs-datepicker to the CodeSandbox dependencies:

and if you are doing this project locally, run

npm install vuejs-datepicker

or

yarn add vuejs-datepicker

in the command line, from the root of your project.

Now we can import the vuejs-datepicker component, and we list it in the components object, to make it available inside the template.

<script>
  import Datepicker from 'vuejs-datepicker'

  export default {
    //...
    components: {
      Datepicker,
    },
  }
</script>

Here’s the usage in the template, we simply pass it a v-model attribute, and it’s already working as we suppose: whenever a user chooses a date, it sets the new date in the component state.

<datepicker v-model="date"></datepicker>

The component state must be updated to have the date property too, which we initialize first to the current date (today):

export default {
  //...
  data: function () {
    return {
      date: new Date(),
      category: this.categories[0],
      amount: 0,
    }
  },
}

We’re almost done! Now we just need to implement the handleClick method, which is triggered when we press the Add button:

methods: {
  handleClick: function() {
    if (!this.date) {
      alert('Enter a date')
      return
    }
    if (!this.amount) {
      alert('Enter an amount')
      return
    }

    this.$emit('addBill', {
      date: this.date,
      category: this.category,
      amount: parseInt(this.amount, 10)
    })
  }
}

we check if the values are filled, and we emit the addBill event to the parent, with as parameter the bill object, which contains the date, the category and the amount.

We use the parseInt() function to remove any kind of leading zero or space in the amount value.

In the App component, here’s how addBill is implemented.

First, we tell the AddBill component to pass up the addBill event when it occurs, and when this happens, we call the addBill method. I use similar names as this eases my reasoning cross-component.

<AddBill
  v-if="shouldShowAddBill"
  :categories="categories"
  v-on:addBill="addBill"
/>

We only show this component if the shouldShowAddBill state property is true. And this will be handled later, now we just add it and set it to true, so we can see our work.

I didn’t include this component in the template yet, so now we need to reorganize a bit the layout of the components. We only want to show this when shouldShowAddCategory is false, so we add it into the v-else, and we create another v-else to make the rest of the app (the navbar, the list, and the chart) show up:

<template>
  <main>
    <AddCategory v-if="shouldShowAddCategory" v-on:addCategory="addCategory" />
    <div v-else>
      <AddBill
        v-if="shouldShowAddBill"
        :categories="categories"
        v-on:addBill="addBill"
      />
      <div v-else>
        <NavBar
          :categories="categories"
          v-on:triggerShowAddCategory="triggerShowAddCategory"
        />
        <div class="container flex">
          <div class="w-1/2">
            <BillsTable />
          </div>
          <div class="w-1/2">
            <Chart />
          </div>
        </div>
      </div>
    </div>
  </main>
</template>

Also, we add the addBill method to the component’s methods list:

addBill(bill) {
  this.bills.push(bill)
  this.shouldShowAddBill = false
}

You should see it working now:

and if you try adding something and pressing “Add”, the view should show this:

Since we add a new bill like we did for the categories, we also need to save it to localStorage, and retrieve it when the app mounts, using the watch and mounted properties in the App component:

watch: {
  bills() {
    localStorage.setItem('bills', JSON.stringify(this.bills))
  },
  categories() {
    localStorage.setItem('categories', JSON.stringify(this.categories))
  }
},
mounted() {
  if (localStorage.getItem('bills')) {
    this.bills = JSON.parse(localStorage.getItem('bills'))
  }

  if (localStorage.getItem('categories')) {
    this.categories = JSON.parse(localStorage.getItem('categories'))
  }

  if (!this.bills.length && !this.categories.length) {
    this.shouldShowAddCategory = true
  }
}

Let’s also add some style to AddBill by adding some Tailwind classes that apply Flexbox and some other styles:

<template>
  <div class="h-100 w-full flex items-center justify-center font-sans">
    <div class="bg-white rounded shadow p-6 m-4 w-full lg:w-3/4 lg:max-w-lg">
      <div class="mb-4">
        <h1 class="text-gray-800">Enter a new bill</h1>
        <p></p>
        <div class="flex mt-4">
          <datepicker
            wrapper-class="shadow appearance-none border rounded text-gray-700"
            input-class="w-full w-full py-2 px-3 mr-4 "
            v-model="date"
          ></datepicker>

          <select v-model="category">
            <option
              v-for="category in categories"
              :value="category"
              :key="category"
            >
              {{ category }}
            </option>
          </select>

          <input
            class="shadow appearance-none border rounded w-full py-2 px-3 mr-4 text-gray-700"
            placeholder="Set amount"
            v-model="amount"
          />
          <button
            class="flex-no-shrink p-2 border-2 rounded bg-teal-700 text-white border-teal hover:text-white hover:bg-teal-700"
            @click="handleClick"
          >
            Add
          </button>
        </div>
      </div>
    </div>
  </div>
</template>

See the app in the current state on CodeSandbox.


Go to the next lesson