Skip to content

Build fintech application with Spring Boot, Vavr and Vue.js (Part 3 – Companies Vue)

Hello! In the last part we implemented backend logic for companies management. This time we will deal with UI side – build Vue.js part. We will observe how to create Vue single file components, connect with API using Axios and how to create nested components and event emitters.

Table of contents

What we will do in this part

This part continues development of Companies part, that we started in previous chapter. So far you should have REST API created and we can proceed to client side application. In this part we will do following things:

  • Write Vue component that creates new company (vendor or customer)
  • Develop Vue component to list companies
  • Learn how to use nested components and what is event emitting
  • Understand how to deal with static assets in Vue.js

So, what are we waiting for? Let do it!

Step 1. Do global styling

First I would like to apply some global styling for our app. The graph below demonstrates what I mean:

Image 1. Application layout

You can see that here I place content inside app-content container to provide some spacing with window’s borders and (in future) with navigation bar. From a techincally point of view, we employ here flexbox concepts, where parent component (app-layout) is flex-container and child component (app-content) is flex-item. Basically there are 2 popular ways to define global styles in Vue.js:

  1. Inside <style> part in root component App.vue
  2. Create “classical” CSS and attach it as static asset to Vue app

Both approaches have advantages and disadvantages and I strongly belive that there is no “one-fits-all” pattern. I can say here, that the 1st solution is unavoidable when you deal with CSS pre-processors, like Less or SCSS. In case you have ordinary CSS stylesheets (like we do), it may be a good idea to have it inside a dedicated CSS file. Also this leads to better decomponsition. What do we need in order to use static CSS stylesheet with Vue.js?

  1. Create normal CSS stylesheet and save it inside public folder
  2. Go to index.html and import it using ordinary <link> tag, where path starts with special expression <%= BASE_URL %>. Here Vue.js inject path to public folder.

Let do it. First create a file public/css/app.css:

.app-layout {
    width: 100%;
    min-height: 100vh;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    flex-direction: row;
}
  
.app-content{
    width: 80%;
    padding: 25px;
}

Next, go to public/index.html and import our stylesheet inside <head> tag:

<link rel="stylesheet" href="<%= BASE_URL %>css/app.css">

Step 2. Create new components

In this part we need two components:

  1. Form to create new company
  2. List to display companies

Basically, Vue components are divided into two groups: components themselves and views. Here views correspond to something like whole screens, while components are smaller parts (like cards or navbars). So, let create two components in src/views folder:

  • CompaniesListView.vue
  • CompaniesNewView.vue

That is what called single file components – which means that all parts (layout, style, scripting) are defined inside single file (compare it with Angular nightmare!). Basic architecture of Vue component looks like this:

<template>
<!-- Layout definition -->
</template>

<script>
export default {
    // here goes JS code
}
</script>

<style>
    /*Scoped style in CSS, Less, SCSS*/
</style>

Note two important moments here:

  • The <template> element can define any layout you want, but can have only one root element. This is a good practice to wrap layout inside single <div> element
  • Style is scoped. That means it is applied to component and downside. For global styling you can use an approach we talked before.

Vue.js is quite simple framework, so let dive into practice and you will see that it is not a rocket science.

Step 3. Define routes

In order to access new views we need to register them in router. Open src/router/index.js and reference components in routes array:

const routes = [
  {
    path: '/companies/new',
    name: 'companies-new',
    component: () => import('../views/CompaniesNewView.vue')
  },
  {
    path: '/companies/list/:type',
    props: true,
    name: 'companies-list',
    component: () => import('../views/CompaniesListView.vue')
  }
]

NB that here we use so-called lazy loading of Vue components. Also you can note props boolean parameter in the second component. Inside URL we use path variable, that in Vue.js is called prop. We need to inform about it Vue.js so we can reference it later inside component.

Step 4. Build CompaniesNewView component

This section is devoted to the development of the component which is responsible for adding new company. We start with hardcoded data and then connect it with API.

Step 4.1 Define layout

We use Buefy framework that offers us reusable components we can use out of the box. Also, it is based on Bulma CSS library and has all its functionality. Go to CompaniesNewView.vue and add following code inside <template> section:

<template>
    <div class="app-content">
        <b-field label="Company name">
            <b-input type="text" v-model="company.name"></b-input>
        </b-field>
        <b-field label="Tax ID">
            <b-input type="text" v-model="company.taxId"></b-input>
        </b-field>
        <div class="app-block">
            <p class="is-size-4">Company type</p>
            <div class="block">
                <b-radio v-model="company.type" native-value="customer">
                    Customer
                </b-radio>
                <b-radio v-model="company.type" native-value="vendor">
                    Vendor
                </b-radio>
            </div>
        </div>
        <div class="app-block">
            <p class="is-size-4">Address</p>
            <b-field label="Address line">
                <b-input type="text" v-model="company.address.addressLine"></b-input>
            </b-field>
            <div class="columns">
                <div class="column">
                    <b-field label="Postal code">
                        <b-input type="text" v-model="company.address.postalCode"></b-input>
                    </b-field>
                </div>
                <div class="column">
                    <b-field label="City">
                        <b-input type="text" v-model="company.address.city"></b-input>
                    </b-field>
                </div>
            </div>
        </div>
        <div class="app-block">
            <b-button v-on:click="createCompany" type="is-primary">Create new company</b-button>
        </div>
    </div>
</template>

It looks overwhelming at first, but basically it has nothing scaring if you are familiar at least with basics of HTML. We just define layout; yes we use custom Buefy components, but they are simple enough. I want to highlight Vue.js specific points instead:

  • v-model directive connects HTML elements with Vue.js data model. That is what called two way data binding. It makes our lives incredibly easy: you can update data model from HTML with no special code, but also, update HTML views from JS with no DOM manipulations.
  • v-on:click directive connect <button> with method createCompany(). It is similar to native onclick event.

Next step is to make it live with JS. Place following code snippet inside script part:

export default {
    data() {
        return {
            company: {
                name: '',
                taxId: '',
                type: 'customer',
                address: {
                    postalCode: '',
                    addressLine: '',
                    city: ''
                }
            }
        }
    },
    methods: {
        createCompany() {
            console.log(this.company)
            this.$buefy.toast.open('Company created')
        }
    }
}
```

We start here with class, so everything is defined in it. Here we have data() function, where we define data models. In our case we have only company entity that mimics the entity we defined on Spring Boot side. Second thing here is createCompany() method that just prints out the resulted data object + shows Buefy toast. Let run our app using serve task in Tasks tab of Vue UI and access localhost:8080/#/companies/new. Open your browser’s developer console, enter some data and press Create New Company button. Output should be printed in console:

Image 2. New company view (with hardcoded data)

Now let connect it with API using Axios.

Step 4.2 Connect with API

First we need to import Axios. You remember, that we created shared Axios base instance, so we need to import it. Before export default{} put import statement:

import {AxiosClient} from '@/http/AxiosClient.js'

Here @ symbol is shortcut for src folder. Also, don’t forget to place curly brackets or you will get an error.

Next you need to refactor the createCompany() method:

createCompany() {
    this.company.userId = 'spring-user'
    AxiosClient.post(`companies/`, this.company)
        .then(res=>{
            this.$buefy.toast.open('Company created')
    }).catch(err=>{
            this.$buefy.toast.open({
                message: 'Cannot create a company',
                type: 'is-danger'
            })
    })
}

Axios is built on the concept of Javascript promises, so here we need to provide two callbacks in then (that is where success callback placed) and in catch (for error handling). Post method uses two params: url and body. We also can supply configuration object as third parameter (where we can place for example auth token). NB that we define only part of URL – Axios concats it with baseUrl that we provided in AxiosClient.js. Now you can run this component with backend:

Image 3. Testing new company with Spring Boot REST API

Step 5. Implement companies list view

In this section we will work with the component that displays list of companies. Remember that we have two types of companies – vendors and customers. Inside section devoted to router we defined a single path with custom prop parameter. We will use it from companies list to retrieve either customers or vendors.

Step 5.1. Define layout

As with new company form, here we start from layout. Place following code inside <template> block:

<template>
<div class="app-content">
    <div>
        <!-- List of cards -->
        <div class="card app-card" v-for="company in companies" :key="company.companyId">
            <div class="card-content columns">
                <div class="column is-two-thirds">
                    <h3 class="title is-3">{{company.name}}</h3>
                    <p>Tax number: {{company.taxId}}</p>
                    <p>Company address: 
                        {{company.address.postalCode}} 
                        {{company.address.addressLine}} 
                        {{company.address.city}}
                    </p>
                </div>
                <div class="column is-one-third has-text-right">
                    <b-button 
                        type="is-danger is-fullwidth" 
                        v-on:click="removeCompany(company.companyId)">Remove</b-button>
                </div>
            </div>
        </div>
    </div>
</div>
</template>

Alongside with familiar v-on:click directive we see here v-for – one of fundamental directives in Vue.js. Basically this component is the list of cards. So we need to iterate through an array of companies we received from REST API and create separate card for each entity. That is what v-for does. First we define a single entity of array (company in companies) and then reference it from card. Also don’t forget to add :key parameter!

Now let try to test it with hardcoded data. Add following code snippet inside <script> part of Vue component:

export default {
    data() {
        return {
            companies: [
                {companyId: 1, name: 'Lenovo', taxId: '123456', address: {postalCode: '1234', city: 'Praha', addressLine: 'Prokopova 12'}},
                {companyId: 2, name: 'Xiaomi', taxId: '123456', address: {postalCode: '1234', city: 'Praha', addressLine: 'Prokopova 12'}},
                {companyId: 3, name: 'Starbucks', taxId: '123456', address: {postalCode: '1234', city: 'Praha', addressLine: 'Prokopova 12'}}
            ]
        }
    },
    methods: {
        removeCompany(id){
            console.log('Remove company with id: ' + id)
        }
    }
}

Now run app and access localhost:8080/#/companies/list/vendors (it will be same for customers however):

Image 4. Vendors view (with hardcoded data)

Before we will connect this component with API, let do some refactoring and decompose card inside a separate component.

Step 5.2. Decompose card in separate component

This will be done in two steps. First create a component src/components/CompanyCard.vue:

<template>
  <div class="card app-card">
    <div class="card-content columns">
      <div class="column is-two-thirds">
        <h3 class="title is-3">{{company.name}}</h3>
        <p>Tax number: {{company.taxId}}</p>
        <p>
          Company address:
          {{company.address.postalCode}}
          {{company.address.addressLine}}
          {{company.address.city}}
        </p>
      </div>
      <div class="column is-one-third has-text-right">
        <b-button type="is-danger is-fullwidth" v-on:click="removeCompany">Remove</b-button>
      </div>
    </div>
  </div>
</template>

<script>
export default {
    props: {
        company: {
            type: Object
        }
    },
    methods: {
        removeCompany(){
            this.$emit('remove-clicked', this.company.companyId)
        }
    }
}
</script>

Basically, we moved here layout defintion from global list component. NB that we use here props to inject data (company object). We also defined emitter in removeCompany() but we will talk about it a bit later.

Now we can use this component inside CompaniesListView.vue. First, add import statement before class:

import CompanyCard from '@/components/CompanyCard.vue'

Then register this component in components property:

components: { CompanyCard },

Now you can reference it inside layout. Let refactor it and use CompanyCard:

<template>
<div class="app-content">
  <div>
	<CompanyCard v-for="company in companies" 
	v-bind:company="company"
	:key="company.companyId"/>
  </div>
</div>
</template>

Looks much better! We inject Company object using v-bind directive.

Step 5.3 Connect with API

We need to define here a custom method updateCompaniesList() that will call API and updates companies[] array:

updateCompaniesList(){
    let userId = 'spring-user'
    let query = 'companies/' + this.type + '/' + userId
    AxiosClient.get(`${query}`).then(response=>{
        this.companies = response.data
    }).catch(err=>{
        console.log(err)
    })
}

We hardcoded userId variable for now. NB that in order to access response’s body we call response.data. We also reference props type like any other data object using this.type.

Next we need to call it inside created method, that is a Vue.js event hook, called when components are created:

created() {
    this.updateCompaniesList()
}

Run our app and now we can see that data for vendors and customers is different, as it is received from API:

Image 6. Test vendors list with Spring Boot REST API

Step 5.4 Removing company

Last thing I want to handle in this part is how to remove a company from list. You remember that we created a custom emitter inside card component, that sends company ID to parent component. Let connect it now with removeCompany method of list component:

<template>
    <div class="app-content">
        <div>
            <CompanyCard v-for="company in companies" 
                v-bind:company="company"
                v-bind:remove-clicked="removeCompany"
                :key="company.companyId"/>
            
        </div>
    </div>
</template>

We do it with same v-bind directive, providing name of emit event we created inside company card. Now we can reference value inside removeCompany method:

removeCompany(val) {
    let url = 'companies/' + val
    AxiosClient.delete(`${url}`)
        .then(response=>{
            this.companies.filter(c => {return c.companyId !== val})
    }).catch(err=>{
        console.log(err)
    })
}

We reference company ID that is injected as a method’s argument and build url. Than we call DELETE method on server. If company was successfully deleted, we use filter method to remove it from client side’s array.

So this is basically all about Vue.js part. In next tutorial we will continue with API development and do invoices and bills management on Spring Boot side. Meanwhile, follow me in socials for updates! Have a nice day!

Copy link
Powered by Social Snap