Creating a Simple Vue.js Expense Tracker: A Beginner’s Guide

Written by

in

Managing finances can be a daunting task, and keeping track of expenses is often the first step towards financial control. While complex financial software exists, creating a simple expense tracker can be a fantastic learning experience for web developers, especially those starting with Vue.js. This project allows you to understand core Vue.js concepts like data binding, component composition, and event handling in a practical, hands-on way. Building an expense tracker not only sharpens your coding skills but also provides a useful tool for personal or project-related financial management. In this guide, we’ll walk through the process of building a basic expense tracker, step by step, from setting up the project to deploying it.

Why Build an Expense Tracker with Vue.js?

Vue.js is a progressive JavaScript framework that’s known for its approachable learning curve and flexibility. It’s an excellent choice for this project because:

  • Component-Based Architecture: Vue.js encourages breaking down your application into reusable components, making your code organized and maintainable.
  • Data Binding: Vue.js simplifies the process of updating the UI when the underlying data changes, and vice-versa.
  • Ease of Use: Vue.js has a clear and concise syntax, making it easier to learn and write code compared to some other frameworks.
  • Progressive Framework: You can integrate Vue.js into existing projects incrementally, making it ideal for both small and large-scale applications.

By building an expense tracker, you’ll gain practical experience in these key areas, which are fundamental to any Vue.js project.

Project Setup and Structure

Before diving into the code, let’s set up our development environment and project structure. We’ll be using Vue CLI (Command Line Interface) to scaffold our project, which provides a convenient way to start a new Vue.js application with pre-configured settings.

Prerequisites

  • Node.js and npm (or yarn): Make sure you have Node.js and npm (Node Package Manager) or yarn installed on your system. You can download them from https://nodejs.org/.
  • Code Editor: Any code editor, such as Visual Studio Code, Sublime Text, or Atom, will work.

Step-by-Step Setup

  1. Install Vue CLI: Open your terminal or command prompt and run the following command to install Vue CLI globally:
    npm install -g @vue/cli
  2. Create a New Project: Navigate to the directory where you want to create your project and run:
    vue create expense-tracker

    You’ll be prompted to select a preset. Choose the “Default (Vue 3) ([Vue 3] babel, eslint)” option. This will set up a basic Vue 3 project with Babel for JavaScript transpilation and ESLint for code linting.

  3. Navigate to the Project Directory: Once the project is created, navigate into the project directory:
    cd expense-tracker
  4. Run the Development Server: Start the development server with:
    npm run serve

    This will start a local development server, usually at http://localhost:8080/. You can open this in your web browser to see the default Vue.js welcome page.

Project Structure

The Vue CLI creates a standard project structure. Here’s a brief overview:

  • public/: Contains static assets like index.html (the main HTML file).
  • src/: This is where your source code will reside:
    • assets/: Contains static assets such as images, fonts, and other media.
    • components/: Where you’ll create reusable Vue components.
    • App.vue: The root component of your application.
    • main.js: The entry point of your application, where Vue is initialized.
  • package.json: Contains project metadata and dependencies.
  • vue.config.js: Configuration file for Vue CLI.

Building the Expense Tracker Components

Now, let’s start building the components of our expense tracker. We’ll create three main components:

  • ExpenseForm.vue: This component will handle the input form for adding new expenses.
  • ExpenseList.vue: This component will display the list of expenses.
  • App.vue: The main component that will orchestrate the other components.

1. ExpenseForm.vue

This component will contain a form with fields for expense description, amount, and date. It will also have a button to submit the expense.

Create a new file named ExpenseForm.vue in the src/components/ directory. Add the following code:

<template>
 <div class="expense-form">
 <h3>Add Expense</h3>
 <form @submit.prevent="addExpense">
 <div class="form-group">
 <label for="description">Description:</label>
 <input type="text" id="description" v-model="description" required>
 </div>
 <div class="form-group">
 <label for="amount">Amount:</label>
 <input type="number" id="amount" v-model.number="amount" required>
 </div>
 <div class="form-group">
 <label for="date">Date:</label>
 <input type="date" id="date" v-model="date" required>
 </div>
 <button type="submit">Add Expense</button>
 </form>
 </div>
</template>

<script>
 export default {
 data() {
 return {
 description: '',
 amount: '',
 date: ''
 };
 },
 methods: {
 addExpense() {
 if (this.description.trim() === '' || this.amount === '' || this.date === '') {
 alert('Please fill in all fields.');
 return;
 }

 const expense = {
 id: Date.now(), // Generate a unique ID
 description: this.description,
 amount: parseFloat(this.amount),
 date: this.date
 };

 this.$emit('expense-added', expense); // Emit an event to the parent component

 // Clear the form
 this.description = '';
 this.amount = '';
 this.date = '';
 }
 }
 };
</script>

<style scoped>
 .expense-form {
 border: 1px solid #ccc;
 padding: 20px;
 margin-bottom: 20px;
 border-radius: 5px;
 }

 .form-group {
 margin-bottom: 15px;
 }

 label {
 display: block;
 margin-bottom: 5px;
 font-weight: bold;
 }

 input[type="text"], input[type="number"], input[type="date"] {
 width: 100%;
 padding: 8px;
 border: 1px solid #ccc;
 border-radius: 4px;
 box-sizing: border-box;
 margin-bottom: 10px;
 }

 button {
 background-color: #4CAF50;
 color: white;
 padding: 10px 20px;
 border: none;
 border-radius: 4px;
 cursor: pointer;
 }

 button:hover {
 background-color: #3e8e41;
 }
</style>

Explanation:

  • <template>: Defines the structure of the component, including the form elements.
  • data(): Initializes the data properties (description, amount, and date) to hold the form input values.
  • v-model: Two-way data binding that links the input fields to the data properties. Changes in the input fields automatically update the data properties and vice versa. The .number modifier ensures that the amount is treated as a number.
  • @submit.prevent="addExpense": Attaches an event listener to the form’s submit event. .prevent prevents the default form submission behavior (page reload).
  • addExpense(): The method that is called when the form is submitted. It creates an expense object and emits an expense-added event with the expense data to the parent component.
  • this.$emit('expense-added', expense): Emits a custom event named expense-added, passing the new expense data as an argument. This event will be listened to by the parent component (App.vue).
  • <style scoped>: Styles specific to this component. scoped means the styles will only apply to this component.

2. ExpenseList.vue

This component will display the list of expenses. It will receive the expenses data from the parent component and render them in a table or a list format.

Create a new file named ExpenseList.vue in the src/components/ directory. Add the following code:

<template>
 <div class="expense-list">
 <h3>Expenses</h3>
 <table v-if="expenses.length > 0">
 <thead>
 <tr>
 <th>Description</th>
 <th>Amount</th>
 <th>Date</th>
 </tr>
 </thead>
 <tbody>
 <tr v-for="expense in expenses" :key="expense.id">
 <td>{{ expense.description }}</td>
 <td>${{ expense.amount.toFixed(2) }}</td>
 <td>{{ expense.date }}</td>
 </tr>
 </tbody>
 </table>
 <p v-else>No expenses added yet.</p>
 </div>
</template>

<script>
 export default {
 props: {
 expenses: {
 type: Array,
 required: true
 }
 }
 };
</script>

<style scoped>
 .expense-list {
 border: 1px solid #ccc;
 padding: 20px;
 border-radius: 5px;
 }

 table {
 width: 100%;
 border-collapse: collapse;
 margin-top: 10px;
 }

 th, td {
 border: 1px solid #ddd;
 padding: 8px;
 text-align: left;
 }

 th {
 background-color: #f2f2f2;
 }
</style>

Explanation:

  • <template>: Contains the HTML structure of the component.
  • props: { expenses: { type: Array, required: true } }: Defines a prop named expenses that the component expects to receive from its parent. The type is specified as Array, and required is set to true.
  • v-if="expenses.length > 0": Conditionally renders the table if the expenses array has items.
  • v-for="expense in expenses" :key="expense.id": Iterates over the expenses array and renders a table row for each expense. The :key="expense.id" is crucial for Vue to efficiently update the DOM when the data changes.
  • {{ expense.description }}, {{ expense.amount.toFixed(2) }}, {{ expense.date }}: Displays the expense details. toFixed(2) formats the amount to two decimal places.
  • <p v-else>No expenses added yet.</p>: Displays a message when there are no expenses.

3. App.vue (Main Component)

This is the main component that orchestrates the other components. It will hold the expenses data and pass it to the ExpenseList component. It will also listen for the expense-added event from the ExpenseForm component and update the expenses data accordingly.

Open src/App.vue and replace its content with the following code:

<template>
 <div id="app">
 <h1>Expense Tracker</h1>
 <ExpenseForm @expense-added="addExpense" />
 <ExpenseList :expenses="expenses" />
 </div>
</template>

<script>
 import ExpenseForm from './components/ExpenseForm.vue';
 import ExpenseList from './components/ExpenseList.vue';

 export default {
 components: {
 ExpenseForm, // Register the ExpenseForm component
 ExpenseList // Register the ExpenseList component
 },
 data() {
 return {
 expenses: [] // Initialize the expenses array
 };
 },
 methods: {
 addExpense(expense) {
 this.expenses.push(expense); // Add the new expense to the expenses array
 }
 }
 };
</script>

<style>
 #app {
 font-family: Avenir, Helvetica, Arial, sans-serif;
 -webkit-font-smoothing: antialiased;
 -moz-osx-font-smoothing: grayscale;
 text-align: center;
 color: #2c3e50;
 margin-top: 60px;
 }

 h1 {
 margin-bottom: 20px;
 }
</style>

Explanation:

  • <template>: Contains the HTML structure.
  • import ExpenseForm from './components/ExpenseForm.vue'; and import ExpenseList from './components/ExpenseList.vue';: Imports the ExpenseForm and ExpenseList components.
  • components: { ExpenseForm, ExpenseList }: Registers the imported components so they can be used in the template.
  • data() { return { expenses: [] }; }: Initializes the expenses array in the component’s data. This array will hold the expense objects.
  • @expense-added="addExpense": Listens for the expense-added event emitted by the ExpenseForm component and calls the addExpense method when the event is triggered.
  • <ExpenseList :expenses="expenses" />: Passes the expenses array as a prop to the ExpenseList component.
  • addExpense(expense) { this.expenses.push(expense); }: This method is called when the expense-added event is emitted. It receives the new expense object as an argument and adds it to the expenses array.

Putting It All Together

Now that we have created all the components, let’s see how they interact with each other.

  1. The user enters the expense details (description, amount, date) in the ExpenseForm component.
  2. When the user submits the form, the addExpense method in ExpenseForm is called. It creates an expense object and emits an expense-added event.
  3. The App.vue component listens for the expense-added event. When it receives the event, it calls the addExpense method in its own script.
  4. The addExpense method in App.vue adds the new expense object to the expenses array.
  5. The ExpenseList component receives the expenses array as a prop and renders the expenses in a table format.

To see the result, open your web browser and go to http://localhost:8080/. You should see the expense tracker application. You can now add expenses, and they will be displayed in the expense list.

Common Mistakes and How to Fix Them

When building Vue.js applications, especially for beginners, there are some common mistakes that can occur. Here are some of them and how to fix them:

  • Incorrect Data Binding:
    • Mistake: Not using v-model correctly, or forgetting to initialize data properties in the data() function.
    • Fix: Ensure you use v-model for two-way data binding in input fields and initialize all data properties in the data() function. For example, data() { return { message: '' } }.
  • Component Registration Issues:
    • Mistake: Not importing and registering components in the parent component.
    • Fix: Import components using the import statement (e.g., import MyComponent from './MyComponent.vue';) and register them in the components object of the parent component (e.g., components: { MyComponent }).
  • Prop Issues:
    • Mistake: Incorrectly defining props in child components or not passing props from parent components.
    • Fix: Define props correctly using the props option in the child component. Make sure to specify the type and, if required, the required property. In the parent component, pass the prop to the child component using the colon syntax (e.g., <MyComponent :my-prop="myData" />).
  • Event Handling Problems:
    • Mistake: Incorrectly emitting or listening to events between components.
    • Fix: Use this.$emit('event-name', data) in the child component to emit an event. In the parent component, use the @event-name="methodName" syntax to listen for the event.
  • CSS Scope Issues:
    • Mistake: Not using scoped in the <style> tag, leading to global CSS rules that can unintentionally affect other components.
    • Fix: Use the scoped attribute in the <style> tag (e.g., <style scoped>) to ensure that the CSS rules only apply to the current component.
  • Incorrect Conditional Rendering:
    • Mistake: Using v-if or v-show incorrectly.
    • Fix: Use v-if when the element should be conditionally rendered and unmounted from the DOM. Use v-show when the element should be conditionally displayed, but always remains in the DOM (it changes the CSS display property).
  • Key Attributes in v-for Loops:
    • Mistake: Missing or using incorrect key attributes in v-for loops.
    • Fix: Always provide a unique key attribute for each item in a v-for loop. The key should be a unique identifier (e.g., the item’s ID).

Enhancements and Next Steps

This simple expense tracker provides a solid foundation. Here are some ideas for further enhancements:

  • Data Persistence: Implement local storage or integrate a backend to save and load expense data. This ensures the data persists even when the user closes the browser.
  • Expense Categories: Add expense categories to the form and display expenses grouped by category. This allows for better organization and analysis of spending habits.
  • Summary and Reporting: Add a summary section to display the total expenses, categorized expenses, and perhaps a monthly breakdown.
  • Filtering and Sorting: Implement filtering options to search for expenses by date, description, or amount. Add sorting capabilities to sort expenses by date, amount, or description.
  • User Interface Improvements: Enhance the user interface with better styling, responsive design, and more user-friendly interactions.
  • Validation: Add more robust input validation to ensure data integrity.
  • Testing: Write unit tests to ensure the application’s components work as expected.
  • Deployment: Deploy the application to a platform like Netlify or Vercel so it can be accessed by others.

Key Takeaways

Building a simple expense tracker with Vue.js is an excellent way to learn the fundamentals of the framework. You’ve learned how to set up a Vue.js project, create and organize components, handle data binding, manage events, and pass data between components using props and events. You’ve also seen how to apply styling using scoped CSS, and how to conditionally render content. The modular structure of Vue.js promotes code reusability and maintainability, making it easier to scale and enhance your application. With the knowledge gained from this project, you can now approach more complex Vue.js projects with confidence, and continue to build your skills. Remember to practice, experiment, and refer to the official Vue.js documentation for further learning and exploration.

As you continue your journey with Vue.js, keep in mind that the framework is constantly evolving. Staying updated with the latest versions, reading the official documentation, and exploring community resources are vital for your growth as a developer. Embrace the challenges, learn from your mistakes, and celebrate your successes. Building projects like this expense tracker is not just about writing code; it’s about problem-solving, creativity, and the joy of bringing your ideas to life. The possibilities are endless, and with each project, you’ll become more proficient and confident in your abilities. Happy coding!