Now we have introduced the Product Details page, we need a way to access that Product's information so we can display it on the frontend. The problem is, passing the whole product via the router link won't work and is not a scaleable approach.
Instead, we need a central place to manage the data state and to give the Product Details component the ability to query the state based on the dynamic ID we pass in the URL.
Let's first visualise how the Vuex store will fit in to our project:
Key points:
getters
commit
updates to the store to make changes to the dataIn your index.html
file, add the following script tag above main.js
:
<script src=" https://unpkg.com/vuex@3.6.2/dist/vuex.js"></script>
Add three new objects:
const products = {
state: {
products: [
{
productId: 1,
name: 'Boston Fern',
description:
'Nephrolepis exaltata, known as the sword fern or Boston fern, is a species of fern in the family Lomariopsidaceae native to tropical regions throughout the world.',
features: ['Moisture loving', 'Easy care', 'Dislikes direct sun'],
stockLevel: 5,
// add rest of product JSON here
},
],
},
getters: {},
};
const cart = {
state: {
cart: [],
},
mutations: {},
getters: {},
};
const business = {
state: {
brand: 'Plants Direct',
address: '21 Sussex Gardens',
city: 'London',
postcode: 'SW1 01L',
},
getters: {},
};
Create a new Vuex store and pass in the modules you created above:
const store = new Vuex.Store({
modules: {
products,
cart,
business,
},
});
Pass the store into your app component:
var app = new Vue({
router,
store,
el: '#app',
// other fields omitted
Remove {{this.cart.length}}
from the home page.
Verify your site is now loading. It won't show any products or other information just yet. We'll fix this below.
As you'll know, all of our components use data from the main app component, but now we want to change this so they get the data from our newly created store. Let's fix the issues one by one:
In the business module, add the following getters:
// Business module
getters: {
getFullAddress(state) {
return `${state.address}, ${state.city}, ${state.postcode}`;
},
getBrand(state) {
return state.brand;
},
},
The state
parameter here is somewhat abstract as we don't actually pass in the state when using the getter methods. In this instance, it refers to the current data state of the store.
In your app component, modify the computed methods as follows:
// app component
computed: {
footerCopyrightNotice() {
return '2021 ' + store.getters.getBrand;
},
footerAddress() {
return 'Our address: ' + store.getters.getFullAddress;
},
},
This should fix the footer output.
In order to access our store, we now need to use getter
methods that we create in the store modules. These methods can only read from the store, and can't modify or update it. In this case, getBrand
and getFullAddress
belong to the business module and return the brand name and the address.
We'll be using this getter pattern a good deal to fix the other data issues, so take a moment to review your changes to understand how it works.
Currently our products aren't displaying. This is because the Home component needs to grab them from the store and pass them down to the Product component to render them.
Head to your index.html
file and remove everything from router view:
<!-- index.html -- >
<router-view />
Next up, add the getNProducts
method to the Product store's getters object. This will allow us to get n number of properties for display on the home page:
// Product store
getters: {
getNProducts: (state) => (n) => {
return state.products.filter((element, index) => index < n);
},
},
Add a method to the Home component to get the products and use this in the template to loop through each product. Also, while you're there, remove the add-to-cart
event as we'll no longer need it.
// Home component
methods: {
getProducts(n) {
return this.$store.getters.getNProducts(n);
},
},
<!-- Home component -->
<product
v-for="product in getProducts(3)"
v-bind:key="product.productId"
v-bind:product="product"
></product>
Don't forget to remove the event from the router link in the Product component:
// Product component
<router-link
v-bind:to="{ name: 'productDetails', params: {productId: product.productId} }"
v-on:add-to-cart="addToCart" // remove this line
class="anchor--button"
></router-link>
Remove the updateCart
method for now:
// Product component
// remove this method
updateCart(productId) {
this.$emit('update-cart', productId);
},
This should now load the products onto the home page.
Currently the text changes on the Product's 'Add to cart' button, but we're not committing anything to the store and changing the cart.
Cut the updateCart
method from your app component and move it to the Cart modules's mutations object. This is the same solution you wrote for the coding challenge that adds/removes a Product ID from the cart.
It could look something like:
// Cart module
mutations: {
updateCart(state, productId) {
if (!state.cart.includes(productId)) {
return state.cart.push(productId);
}
state.cart.splice(state.cart.indexOf(productId), 1);
},
},
Be sure to remove the empty methods object from your app component.
In your Product component, replace the addToCart
method with the following version:
// Product component
addToCart(product) {
product.addedToCart = !product.addedToCart;
this.$store.commit('updateCart', product.productId);
},
Here we perform the NOT trick on the product then we use commit
to commit the change to the store by passing the event name and the new value.
Replace the cart length code in the header with:
<!-- index.html -->
<div class="cart">Cart ({{$store.getters.getCartLength}})</div>
The $store
object is an object Vue exposes so we can query getters from a page such as the index page.
This should be enough to get your cart up and running again!
The penultimate fix to get Vuex fully up and running is to get the Product Details to show up on the Product page. To do this, we'll use the dynamic ID we are passing via the router link to find the product by ID in the store. We'll then add this product to the local state via a lifecycle method so we can output the details. Let's go through it bit by bit:
Firstly, add the method to find a product by ID to the Product module's getter methods object:
// Product module
getters: {
getNProducts: (state) => (n) => {
return state.products.filter((element, index) => index < n);
},
getProduct: (state) => (productId) => {
return state.products.find(
(product) => product.productId === parseInt(productId)
);
},
},
This is quirky construct like the getNProducts
function that is a function that returns a function that returns the results of the find method. We need this in order for the state and the productId
to be passed in.
Next, we need to use the mounted
lifecycle method to call the method we created above. We then assign the results to the local data state. We use this.$route.params.productId
, which is another handy object Vue exposes that allows us to get the Product ID that we specified in the router link in Part 6.
// Main.js
const ProductDetails = Vue.component('Product-Details', {
template: `
// Template omitted
`,
data() {
return {
product: {},
};
},
mounted() {
this.product = this.$store.getters.getProduct(this.$route.params.productId);
},
});
A lifecycle method is a group of events that fire at predictable times during the creation of a Vue component. In our case, the mounted
method can be used to request data once the component has been mounted (added) to the DOM.
The last step is to add in some methods to add the product to cart and to fetch the second image in the images array:
// Product Details component
methods: {
getImage(product) {
return product.images[1].imageSrc;
},
addToCart(product) {
product.addedToCart = !product.addedToCart;
this.$store.commit('updateCart', product.productId);
},
},
You should now see the Product Details and be able to add/remove the product to/from the cart! In addition, the state should hold when you return to the home page. Add the product to the cart, return home, and you should still see the item in the cart. You should also see that the product's 'Add to cart' button has changed to 'Remove from cart'.
You may notice that there's an error in the console on the Property Details page:
vue:6 TypeError: Cannot read property '1' of undefined
at a.getImage (main.js:234)
Why is this error being thrown yet the image still loads?
How can you fix it?