DEV Community

Mindaugas Januška
Mindaugas Januška

Posted on • Edited on

Part 3: Edit project to meet requirements

Table Of Contents

What is done so far?

Hence, so far I have done:

  • created a new project on GitHub.

  • cloned remote GitHub repository to the local computer.

  • created template Vue 3 project.

  • pushed template Vue 3 code from local computer to the remote repository on GitHub.

But this project does nothing. It is just standard template generated by @vue/cli.

What I am going to build now?

Now I am going to create a demo website with a list of products.

To recap from Step 0 following conditions should be met:

  • Loaded page should have several demo products stored.
  • It should be available to add, edit and delete a product.
  • Backend or database shouldn't be used, but products should be stored for each particular user who uses demo website.
  • Product code, name and price should be available to add manually by the user.
  • When a price of the product is added by the user, then subtotal price should be displayed immediately with a VAT of 21% added.
  • Values should be reactive.
  • Input fields should have validation rules, so that invalid values wouldn't be accepted.
  • Demo website should have only 2 pages. Main (index) page to display, add, delete and edit products and billing page to display a list of products and subtotal price of all the products.

So, let's get started on the action!

Install required packages

As you probably noticed earlier I have installed default Vue 3 project. It means, that vue-router and vue-store wasn't installed by default. I have done this intentionally, so that I have to install and register them manually. When things are done by default it is not always clear what is going on and how it works behind the scenes. By installing/adding sofware manually it is always possible to learn and better understand principles of these software.
Hence let's install and register following packages required for this project:

  1. vue-router
  2. vue-store
  3. vee-validate form validation library for Vue.js
  4. Yup data validation library which will be used on top of vee-validate
  5. Bulma CSS
npm i vue-router@next
Enter fullscreen mode Exit fullscreen mode
npm i vuex@next
Enter fullscreen mode Exit fullscreen mode
npm install bulma
Enter fullscreen mode Exit fullscreen mode
npm install vee-validate@next
Enter fullscreen mode Exit fullscreen mode
npm i yup
Enter fullscreen mode Exit fullscreen mode

After the instalation package.json file was added with above mentioned packages:

npm i packages

Register installed packages

When packages are installed they should be registered to be available to use in the application.
Replace default code in the src/main.js file with the following code:

import { createApp } from 'vue'
import App from './App.vue'
import '../node_modules/bulma/css/bulma.css' // <-- import Bulma CSS
import router from './router' // <-- import router
import store from './store' // <-- import store
createApp(App).use(router).use(store).mount('#app') // <-- use router and store
view raw main.js hosted with ❤ by GitHub

Create Router at /src/router/index.js

Create a new folder and new file at src/router/index.js and paste following code:

import { createWebHistory, createRouter } from 'vue-router'
import BillPage from '@/views/BillPage.vue'
import Products from '@/views/Products.vue'
const routes = [
{
path: '/',
name: 'products',
component: Products
},
{
path: '/billPage',
name: 'BillPage',
component: BillPage
}
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
view raw index.js hosted with ❤ by GitHub

Create vuex at /src/store/index.js

Create a new folder and new file at src/store/index.js and paste following code:

import { createStore } from 'vuex'
export default createStore({
state: {
subtotalBasePrice: null,
subtotalTotalPrice: null
},
mutations: {
SET_SUBTOTAL_BASE_PRICE (state, value) {
state.subtotalBasePrice = value
},
SET_SUBTOTAL_TOTAL_PRICE (state, value) {
state.subtotalTotalPrice = value
}
},
getters: {
getSubtotalBasePrice: state => {
return state.subtotalBasePrice
},
getSubtotalTotalPrice: state => {
return state.subtotalTotalPrice
}
},
actions: {},
modules: {}
})
view raw index.js hosted with ❤ by GitHub

Create file /src/views/Products.vue/:

<template>
<section class="hero is-fullheight">
<div class="hero-head">
<NavBar />
</div>
<div class="hero-body">
<div class="container">
<div class="table container">
<table class="table is-fullwidth">
<thead>
<tr>
<th><abbr title="Code">Code</abbr></th>
<th>Name</th>
<th><abbr title="Base Price">Base Price</abbr></th>
<th><abbr title="Total Price">Total Price</abbr></th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr
v-for="(product, index) in products"
:key="index"
>
<td>
<input v-if="product.isEditMode" type="text" class="input" v-model="product.code">
<span v-else>{{ product.code }}</span>
</td>
<td>
<input v-if="product.isEditMode" type="text" class="input" v-model="product.name">
<span v-else>{{ product.name }}</span>
</td>
<td>
<input v-if="product.isEditMode" type="text" class="input" v-model="product.basePrice">
<span v-else>{{ product.basePrice }}</span>
</td>
<td>{{ product.totalPrice }}</td>
<td>
<button v-if="product.isEditMode" class="button mr-1 my-1 is-small" v-on:click="save(product, index)">save</button>
<button v-if="product.isEditMode" class="button mr-1 my-1 is-small" v-on:click="cancel(product, index)">cancel</button>
<button v-if="product.isEditMode" class="button mr-1 my-1 is-small" v-on:click="remove(product, index)">delete</button>
<button v-if="!product.isEditMode" class="button" v-on:click="edit(product, index)">edit</button>
</td>
</tr>
<tr>
<td>Subtotal:</td>
<td></td>
<td>{{ subtotalBasePrice }}</td>
<td>{{ subtotalTotalPrice }}</td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td v-if="!isNewLine"><button class="button is-success" v-on:click="addNewLine()">Add New</button></td>
</tr>
</tbody>
</table>
</div>
<form v-if="isNewLine" @submit="submit">
<div class="table container">
<table class="table is-fullwidth">
<tbody>
<tr>
<td>
<div class="field">
<div class="control">
<input type="text" class="input" placeholder="code" v-model="code" :error="codeError">
<p class="help is-danger">{{ codeError }}</p>
</div>
</div>
</td>
<td>
<div class="field">
<div class="control">
<input type="text" class="input" placeholder="name" v-model="name" :error="nameError">
<p class="help is-danger">{{ nameError }}</p>
</div>
</div>
</td>
<td>
<div class="field">
<div class="control">
<input type="text" class="input" placeholder="price" v-model="basePrice" :error="basePriceError">
<p class="help is-danger">{{ basePriceError }}</p>
</div>
</div>
</td>
<td>
<div class="field is-grouped">
<div class="control">
<button class="button is-link">Submit</button>
</div>
<div class="control">
<button class="button is-danger" @click='cancelAddNewLine()'>cancel</button>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</form>
<section class="section" v-if="!isNewLine">
<button class="button is-link" v-on:click="next()">Next</button>
</section>
</div>
</div>
<div class="hero-foot">Created by Mindaugas Januška</div>
</section>
</template>
<script>
import { computed, ref, watch } from 'vue'
import { useStore } from 'vuex'
import { useRouter } from 'vue-router'
import { useField, useForm, useResetForm } from 'vee-validate'
import { number, string } from 'yup'
import NavBar from '../components/NavBar.vue'
export default {
components: { NavBar },
setup() {
let products = ref([])
let isNewLine = ref(false)
let taxValue = ref(0.21)
const store = useStore()
const router = useRouter()
const subtotalBasePrice = computed({
get: () => {
return store.getters.getSubtotalBasePrice
},
set: (value) => {
store.commit('SET_SUBTOTAL_BASE_PRICE', value)
}
})
const subtotalTotalPrice = computed({
get: () => {
return store.getters.getSubtotalTotalPrice
},
set: (value) => {
store.commit('SET_SUBTOTAL_TOTAL_PRICE', value)
}
})
const validationss = {
code: number().required().min(3),
name: string().required().min(3),
basePrice: number().required()
}
const { handleSubmit } = useForm({
validationSchema: validationss
})
const submit = handleSubmit((values, { resetForm }) => {
basePrice.value = parseFloat(basePrice.value.replace(',', '.').replace(' ', '')) // convert commas to dots to avoid NaN
const totalPrice = countTax(basePrice.value)
const product = { code: code.value, name: name.value, basePrice: basePrice.value, totalPrice: totalPrice }
products.value.push(product)
localStorage.setItem('products', JSON.stringify(products.value))
isNewLine.value = false
code.value = ''
name.value = ''
basePrice.value = ''
resetForm() // https://vee-validate.logaretm.com/v4/guide/composition-api/handling-forms#handling-submissions
})
const { value: code, errorMessage: codeError } = useField('code')
const { value: name, errorMessage: nameError } = useField('name')
const { value: basePrice, errorMessage: basePriceError } = useField('basePrice')
const resetForm = useResetForm()
function cancelAddNewLine () {
resetForm(); // resets the form
isNewLine.value = false
}
function populateLocalStorageWithDummyData () {
const products = [
{ code: '12345', name: 'T-shirt', basePrice: '14.78' },
{ code: '54321', name: 'Cardigan', basePrice: '34.56' }
]
localStorage.setItem('products', JSON.stringify(products))
localStorage.setItem('productEditIndex', null)
}
function createLocalStorage () {
let products = localStorage.getItem('products')
let tempLocalStorageArr = JSON.parse(localStorage.getItem('products'))
if (!products && !tempLocalStorageArr) { // if localStorage is completely empty (or deleted), then it doesn't has any records, hence 'products' and 'tempLocalStorage' are equal to 'null'
populateLocalStorageWithDummyData()
}
else if (Object.keys(tempLocalStorageArr).length === 0) { // if all items was removed from localStorage, then it has empty object named 'products', but object's length is 0.
populateLocalStorageWithDummyData()
}
}
function setProducts () {
products.value = JSON.parse(localStorage.getItem('products'))
}
function countTotalPrice () {
products.value.forEach(element => {
const totalPrice = countTax(element.basePrice)
element.totalPrice = totalPrice
})
}
function countSubtotal () {
let basePriceTotal = 0
let totalPriceSubtotal = 0
products.value.forEach(element => {
const elementBasePrice = Number(element.basePrice)
const elementTotalPrice = Number(element.totalPrice)
basePriceTotal += elementBasePrice
totalPriceSubtotal += elementTotalPrice
})
subtotalBasePrice.value = Math.round((basePriceTotal + Number.EPSILON) * 100) / 100 // round to 2 decimal places
subtotalTotalPrice.value = Math.round((totalPriceSubtotal + Number.EPSILON) * 100) / 100 // round to 2 decimal places
}
function countTax (param) {
const price = Number(param) // convert string to number
const tax = price * taxValue.value
return Math.round(((price + tax) + Number.EPSILON) * 100) / 100 // round to 2 decimal places
}
function edit (product, index) {
localStorage.setItem('productEditIndex', index)
// this.$set(product, 'isEditMode', true) // will trigger state updates in the reactivity system; 'product.isEditMode = true' doesn't trigger state updates. THIS WAS VALID IN VUE_2
product['isEditMode'] = true // vue_3 edition
}
function cancel (product) {
product.isEditMode = false
products.value = JSON.parse(localStorage.getItem('products'))
localStorage.setItem('productEditIndex', null)
}
function save (product) {
if (typeof product.basePrice === 'string' || product.basePrice instanceof String) {
product.basePrice = parseFloat(product.basePrice.replace(',', '.').replace(' ', '')) // convert commas to dots to avoid NaN. Because you never know if user inputs comma or dot.
}
product.isEditMode = false
localStorage.setItem('products', JSON.stringify(this.products))
localStorage.setItem('productEditIndex', null)
products.value = JSON.parse(localStorage.getItem('products'))
}
function remove (product, index) {
products.value.splice(index, 1)
// product.isEditMode = false
localStorage.setItem('products', JSON.stringify(products.value))
localStorage.setItem('productEditIndex', null)
}
function addNewLine () {
isNewLine.value = true
}
function next () {
localStorage.setItem('products', JSON.stringify(products.value))
router.push('/billPage')
}
createLocalStorage()
setProducts()
countTotalPrice()
countSubtotal()
watch(products, () => {
countTotalPrice()
countSubtotal()
},
{ deep: true }
)
return {
products,
isNewLine,
name,
nameError,
basePrice,
basePriceError,
taxValue,
subtotalBasePrice,
subtotalTotalPrice,
codeError,
code,
createLocalStorage,
setProducts,
countTotalPrice,
countSubtotal,
countTax,
edit,
cancel,
save,
remove,
addNewLine,
submit,
cancelAddNewLine,
next
}
},
}
</script>
view raw Products.vue hosted with ❤ by GitHub

Create file /src/views/BillPage.vue:

<template>
<section class="hero is-fullheight">
<div class="hero-head">
<NavBar />
</div>
<div class="hero-body">
<div class="table container">
<table class="table is-fullwidth">
<thead>
<tr>
<th><abbr title="Code">Code</abbr></th>
<th>Name</th>
<th><abbr title="Base Price">Base Price</abbr></th>
<th><abbr title="Total Price">Total Price</abbr></th>
</tr>
</thead>
<tbody>
<tr
v-for="(product) in products"
:key="product.index"
>
<td>{{ product.code }}</td>
<td>{{ product.name }}</td>
<td>{{ product.basePrice }}</td>
<td>{{ product.totalPrice }}</td>
</tr>
<tr>
<td>Subtotal:</td>
<td></td>
<td>{{ subtotalBasePrice }}</td>
<td>{{ subtotalTotalPrice }}</td>
<td></td>
</tr>
</tbody>
</table>
<button class="button is-success" v-on:click="back()">Back</button>
</div>
</div>
<div class="hero-foot">Created by Mindaugas Januška</div>
</section>
</template>
<script>
import NavBar from '../components/NavBar.vue'
export default {
components: { NavBar },
computed: {
subtotalBasePrice () {
return this.$store.getters.getSubtotalBasePrice
},
subtotalTotalPrice () {
return this.$store.getters.getSubtotalTotalPrice
}
},
data () {
return {
products: null,
taxValue: 0.21
}
},
created () {
this.products = JSON.parse(localStorage.getItem('products'))
},
methods: {
back () {
this.$router.push('/')
}
}
}
</script>
view raw BillPage.vue hosted with ❤ by GitHub

Replace code in /src/App.vue:

<template>
<div id="app">
<router-view/>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
</style>
view raw App.vue hosted with ❤ by GitHub

Replace code in /src/main.js:

import { createApp } from 'vue'
import App from './App.vue'
import '../node_modules/bulma/css/bulma.css' // <-- import Bulma CSS
import router from './router' // <-- import router
import store from './store' // <-- import store
createApp(App).use(router).use(store).mount('#app') // <-- use router and store
view raw main.js hosted with ❤ by GitHub

Create new component at src/components/NavBar.vue

<template>
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="https://bulma.io">
<img src="https://bulma.io/images/bulma-logo.png" width="112" height="28">
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbarBasicExample" class="navbar-menu">
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<a class="button is-primary">
<strong>Sign up</strong>
</a>
<a class="button is-light">
Log in
</a>
</div>
</div>
</div>
</div>
</nav>
</template>
view raw NavBar.vue hosted with ❤ by GitHub

Stage and commit changes

stage-commit changes

Git push code to the remote GitHub repository

git push
Enter fullscreen mode Exit fullscreen mode

Summary

Congrats! Application is ready to be used. Run npm run serve to run the application if it is not running yet.

AWS GenAI LIVE image

Real challenges. Real solutions. Real talk.

From technical discussions to philosophical debates, AWS and AWS Partners examine the impact and evolution of gen AI.

Learn more

Top comments (0)

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay