. As our templates need a single root
element, wrap both of them in an extra container:
const ProductPage = {
name: 'ProductPage',
template: `
`,
components: {
PageNotFound
},
computed: {
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.$route.params.slug];
}
return product;
}
}
};
Viewing this in the browser will render the 404page template and, once the data has
loaded, the product title above that. We now need to update the component to only show
the PageNotFound component when there is no data to show. We could use the existing
product variable with a v-if attribute and, if false, show the error message like so:
However, this would mean that if the user visited the product page without the product
data loading yet, they would see a flash of the 404 information before being replaced with
the product information. This isn't a very good user experience, so we should only show
the error if we are sure the product data has loaded and that there isn't a matching item.
[ 251 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
To combat this, we will create a new variable which will determine if the component
displays. Create a data function in the ProductPage component that returns an object with
a key of productNotFound, set to false. Add a v-if condition to the
element, checking against the new productNotFound variable:
const ProductPage = {
name: 'ProductPage',
template: ``,
components: {
PageNotFound
},
data() {
return {
productNotFound: false
}
},
computed: {
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.$route.params.slug];
}
return product;
}
}
};
The last step is to set the variable to true if a product doesn't exist. As we only want to do
this once the data has loaded, add the code to the $store.state.products check. We are
already assigning the data to the product variable, so we can add a check to see if this
variable exists – if not, change the polarity of our productNotFound variable:
const ProductPage = {
name: 'ProductPage',
template: ``,
[ 252 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
components: {
PageNotFound
},
data() {
return {
productNotFound: false
}
},
computed: {
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.$route.params.slug];
if(!product) {
this.productNotFound = true;
}
}
return product;
}
}
};
Try entering an erroneous string at the end of the URL – you should be faced with our, now
familiar, 404error page.
Displaying product information
With our product loading, filtering, and error-catching in place, we can proceed with
displaying the information we need for our product. Each product could contain one or
many images, and one or many variations and any combination in-between – so we need to
make sure we cater for each of these scenarios.
To see the data available to us, add a console.log(product) just before the return:
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.$route.params.slug];
if(!product) {
this.productNotFound = true;
[ 253 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
}
}
console.log(product);
return product;
}
Open up the JavaScript console and inspect the object that should now be there. Familiarize
yourself with the keys and values available to you. Take note that the images key is an
array and the variations an object, containing a string and a further array.
Before we tackle the variations and images – let's output the simple stuff. What we need to
remember is that every field we output might not exist on every product – so it's best to
wrap it in conditional tags where necessary.
Output the body, type, and vendor.title from the product details. Prepend both the
vendor.title and type with a description of what they are, but make sure you only
render that text if it exists in the product details:
template: `
{{ product.title }}
Manufacturer: {{ product.vendor.title }}
Category: {{ product.type }}
{{ product.body }}
`,
Notice we've got the flexibility to prepend the type and vendor with more user-friendly
names. Once we have our categories and filtering set up, we can link both the vendor and
type to appropriate product listing.
[ 254 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
Viewing this in the browser will reveal the body outputting all HTML tags as text –
meaning we can see them on the page. If you cast your mind back to the beginning of the
book where we were discussing output types, we need to use v-html to tell Vue to render
the block as raw HTML:
template: `
{{ product.title }}
Manufacturer: {{ product.vendor.title }}
Category: {{ product.type }}
`,
Product images
The next step is to output the images for our product. If you are using the bicycles CSV file,
a good product to test with is 650c-micro-wheelset – navigate to this product as it has
four images. Don't forget to go back to your original product to check that it works with
one image.
The images value will always be an array, whether there is one image or 100, so to display
them, we will always need to do a v-for. Add a new container and loop through the
images. Add a width to each image so it doesn't take over your page.
The images array contains an object for each image. This has an alt and source key that
can be input directly into your HTML. There are some instances, however, where the alt
value is missing – if it is, insert the product title instead:
template: `
{{ product.title }}
Manufacturer: {{ product.vendor.title }}
Category: {{ product.type }}
`,
With our images displaying, it would be a nice addition to create a gallery. Shops often
show one big image, with a set of thumbnails underneath. Clicking each thumbnail then
replaces the main image so the user can get a better look at the bigger image. Let's recreate
that functionality. We also need to ensure we don't show the thumbnails if there is only one
image.
We do this, by setting an image variable to the first image in the images array, this is the
one that will form the big image. If there is more than one image in the array, we will show
the thumbnails. We will then create a click method that updates the image variable with the
selected image.
Create a new variable in your data object and update it with the first item from the images
array when the product has loaded. It's good practice to ensure the images key is, in fact,
an array of items before trying to assign a value:
const ProductPage = {
name: 'ProductPage',
template: `
[ 256 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
{{ product.title }}
Manufacturer: {{ product.vendor.title }}
Category: {{ product.type }}
`,
components: {
PageNotFound
},
data() {
return {
productNotFound: false,
image: false
}
},
computed: {
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.$route.params.slug];
this.image = (product.images.length) ? product.images[0] : false;
if(!product) {
this.productNotFound = true;
}
}
console.log(product);
return product;
}
}
};
[ 257 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
Next, update your existing images loop in your template to only display when there is
more than one image in the array. Also, add the first image as the main image in your
template – not forgetting to check whether it exists first:
template: `
{{ product.title }}
Manufacturer: {{ product.vendor.title }}
Category: {{ product.type }}
`,
The last step is to add a click handler to each of the thumbnail images, to update the image
variable when interacted with. As the images will not natively have the cursor: pointer
CSS attribute, it might be worth considering adding this.
[ 258 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
The click handler will be a method that accepts each image in the thumbnail loop as a
parameter. On click, it will simply update the image variable with the object passed
through:
const ProductPage = {
name: 'ProductPage',
template: `
{{ product.title }}
Manufacturer: {{ product.vendor.title }}
Category: {{ product.type }}
`,
components: {
PageNotFound
},
data() {
return {
[ 259 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
productNotFound: false,
image: false
}
},
computed: {
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.$route.params.slug];
this.image = (product.images.length) ? product.images[0] : false;
if(!product) {
this.productNotFound = true;
}
}
console.log(product);
return product;
}
},
methods: {
updateImage(img) {
this.image = img;
}
}
};
Load the product up in your browser and try clicking on any of the thumbnails - you
should be able to update the main image. Don't forget to validate your code on a product
with one image or even zero images, to make sure the user isn't going to encounter any
errors.
Don't be afraid of whitespace and adding new lines for readability. Being
able to easily understand your code is better than the few bytes you
would have saved on file load. When deploying to production, files
should be minified, but during development white space takes
precedence.
[ 260 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
Product variations
With this particular dataset, each of our products contains at least one variation but can
contain several. This normally goes hand-in-hand with the number of images but does not
always correlate. Variations can be things such as color or size.
On our Product object, we have two keys which are going to help us display the
variations. These are variationTypes, which list the names of the variations such as size
and color, and variationProducts,which contains all of the variations. Each product
within the variationProducts object has a further object of variant, which lists all of
the changeable properties. For example, if a jacket came in two colors and each color had
three sizes, there would be six variationProducts, each with two variant properties.
Every product will contain at least one variation, although if there is only one variation, we
may need to consider the UX of the product page. We are going to display our product
variations in both a table and drop-down, so you can experience creating both elements.
Variations display table
Create a new container in your product template that will display the variations. Within
this container, we can create a table to display the different variations of the product. This
will be achieved with a v-for declaration. However, now that you are more familiar with
the functionality, we can introduce a new attribute.
Using a key with loops
When using loops in Vue, it is advised you use an extra attribute to identify each item,
:key. This helps Vue identify the elements of the array when re-ordering, sorting, or
filtering. An example of :key use would be:
{{ item.title }}
The key attribute should be a unique attribute of the item itself and not the index of the
item in the array, to help Vue identify the specific object. More information about using a
key with a loop is available in the official Vue documentation.
We'll be utilizing the key attribute when displaying our variations, but using the barcode
attribute.
[ 261 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
Displaying the variations in a table
Add a table element to your variations container and loop through the items array. For
now, display the title, quantity and price. Add an additional cell that contains a
button with the value of Add to basket. We'll configure that in Chapter 11, Building an Ecommerce Store – Adding a Checkout. Don't forget to add a $ currency symbol in front of your
price, as it's currently just a "raw" number.
Watch out – when using the $ sign within the template literals, JavaScript will try and
interpret it, along with the curly brackets, as a JavaScript variable. To counteract this,
prepend the currency with a backslash – this tells JavaScript that the next character is
literal, and should not be interpreted in any other way:
template: `
{{ product.title }}
Manufacturer: {{ product.vendor.title }}
Category: {{ product.type }}
[ 262 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
{{ variation.quantity }}
\${{ variation.price }}
Add to basket
`,
Although we're displaying the price and quantity, we aren't outputting the actual variant
properties of the variation (such as color). To do this, we are going to need to do some
processing on our variation with a method.
The variant object contains a child object for each variation type, with a name and a value
for each type. They are also stored with a slug-converted key within the object. See the
following screenshot for more details:
[ 263 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
Add a new cell at the beginning of the table that passes the variation to a method titled
variantTitle():
{{ variantTitle(variation) }}
{{ variation.quantity }}
\${{ variation.price }}
Add to basket
Create the new method within your methods object:
methods: {
updateImage(img) {
this.image = img;
},
variantTitle(variation) {
}
}
We now need to construct a string with the title of the variant, displaying all available
options. To do this, we are going to construct an array of each of the types and then join
them into a string.
Store the variants as a variable and create an empty array. We can now loop through the
keys available within the variants object and create a string to output. If you decide to
add HTML into the string, as shown in the following example, we will need to update our
template to output HTML instead of a raw string:
variantTitle(variation) {
let variants = variation.variant,
output = [];
for(let a in variants) {
output.push(`${variants[a].name}: ${variants[a].value}`);
}
}
[ 264 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
Our output array will have an item for each variant, formatted like the following:
["Color: Alloy", "Size: 49 cm"]
We can now join each one together, which transforms the output from an array to a string.
The character, string, or HTML you choose to join it with is up to you. For now, use a /
with spaces on either side. Alternatively, you could use tags to create a new
table cell. Add the join() function and update the template to use v-html:
const ProductPage = {
name: 'ProductPage',
template: `
{{ product.title }}
Manufacturer: {{ product.vendor.title }}
Category: {{ product.type }}
[ 265 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
{{ variation.quantity }}
\${{ variation.price }}
Add to basket
`,
components: {
PageNotFound
},
data() {
return {
productNotFound: false,
image: false
}
},
computed: {
...
},
methods: {
updateImage(img) {
this.image = img;
},
variantTitle(variation) {
let variants = variation.variant,
output = [];
for(let a in variants) {
output.push(`${variants[a].name}: ${variants[a].value}`);
}
return output.join(' / ');
}
}
};
[ 266 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
Attach a click event to the Add to basket button and create a new method on the
component. This method will require the variation object to be passed in, so the correct
one could be added to the basket. For now, add a JavaScript alert() to confirm you have
the right one:
const ProductPage = {
name: 'ProductPage',
template: `
{{ product.title }}
Manufacturer: {{ product.vendor.title }}
Category: {{ product.type }}
{{ variation.quantity }}
\${{ variation.price }}
Add to
[ 267 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
basket
`,
components: {
PageNotFound
},
data() {
return {
productNotFound: false,
image: false
}
},
computed: {
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.$route.params.slug];
this.image = (product.images.length) ? product.images[0] : false;
if(!product) {
this.productNotFound = true;
}
}
console.log(product);
return product;
}
},
methods: {
updateImage(img) {
this.image = img;
},
variantTitle(variation) {
let variants = variation.variant,
output = [];
[ 268 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
for(let a in variants) {
output.push(`${variants[a].name}: ${variants[a].value}`);
}
return output.join(' / ');
},
addToBasket(variation) {
alert(`Added to basket: ${this.product.title} ${this.variantTitle(variation)}`);
}
}
};
Note the template literals used within the alert box – this allows us to use Javascript
variables without having to use string concatenation techniques. Clicking on the Add to
basket button will now generate a popup listing of the name of the product and the
variation clicked.
Displaying variations in a select box
A more common interface pattern on product pages is to have a drop-down list, or select
box, with your variations displayed and available for selecting.
When using a select box, we will have a variation which has either been selected by default
or that the user has interacted with and chosen specifically. Because of this, we can change
the image when the user changes the select box and display other pieces of information
about the variant on the product page, including price and quantity.
We won't be relying on passing through the variant to the addToBasket method, as it will
exist as an object on the product component.
Update your element to be a , and the to an . Move the
button outside of this element and remove the parameter from the click event. Remove
any HTML from the variantTitle() method. Because it is now inside a select box it is
not required:
Add to basket
The next step is to create a new variable available to use on the component. In a similar vein
to the images, this will be completed with the first item of the variationProducts array
and updated when the select box changes.
Create a new item in the data object, titled variation. Populate this variable when the
data is loaded into the product computed variable:
const ProductPage = {
name: 'ProductPage',
template: `...`,
components: {
PageNotFound
},
data() {
return {
productNotFound: false,
image: false,
variation: false
}
},
computed: {
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.$route.params.slug];
this.image = (product.images.length) ? product.images[0] : false;
this.variation = product.variationProducts[0];
if(!product) {
this.productNotFound = true;
}
}
console.log(product);
return product;
}
[ 270 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
},
methods: {
...
}
};
Update the addToBasket method to use the variation variable of the ProductPage
component and not rely on a parameter:
addToBasket() {
alert(`Added to basket: ${this.product.title} ${this.variantTitle(this.variation)}`);
}
Try clicking the Add to basket button – it should add the first variation, regardless of what
is selected in the dropdown. To update the variable on change, we can bind the
variations variable to the select box – in the same way, that we did our textbox filtering
at the beginning of this book.
Add a v-model attribute to the select element. We will also need to tell Vue what to bind
to this variable when selecting. By default, it will do the contents of the , which is
currently our custom variant title. However, we want to bind the whole variation object.
Add a :value property to the element:
Add to basket
Changing the select box and clicking the Add to basket button will now produce the
correct variation. This method gives us much more flexibility over displaying the variations
in a table.
[ 271 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
It allows us to display variation data in other places on the product. Try adding the price
next to the product title and the quantity within the meta container:
template: `
{{ product.title }} - \${{ variation.price }}
Manufacturer: {{ product.vendor.title }}
Category: {{ product.type }}
Quantity: {{ variation.quantity }}
Add to basket
[ 272 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
`,
These two new attributes will update when changing the variation. We can also update the
image to the selected variation if it has one. To do this, add a watch object to your
component, which watches the variation variable. When updated, we can check if the
variation has an image and, if so, update the image variable with this property:
const ProductPage = {
name: 'ProductPage',
template: `...`,
components: {
...
},
data() {
...
},
computed: {
...
},
watch: {
variation(v) {
if(v.hasOwnProperty('image')) {
this.updateImage(v.image);
}
}
},
methods: {
...
}
};
[ 273 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
When using watch, the function passes the new item as the first parameter. Rather than
referring to the one on the component, we can use this to gather the image information.
Another enhancement we can make is to disable the Add to basket button and add a note
in the dropdown if the variation is out of stock. This information is gathered from the
variation quantity key.
Check the quantity and, if less than one, display an out of stock message in the select box
and disable the Add to basket button using the disabled HTML attribute. We can also
update the value of the button:
template: `
{{ product.title }} - \${{ variation.price }}
Manufacturer: {{ product.vendor.title }}
Category: {{ product.type }}
Quantity: {{ variation.quantity }}
[ 274 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
{{ (variation.quantity) ? 'Add to basket' : 'Out of stock' }}
`,
If using the bicycles.csv dataset, the Keirin Pro Track Frameset product
(/#/product/keirin-pro-track-frame) contains several variations, some without
stock. This allows you to test the out of stock functionality along with the image
changing.
Another thing we can do to the product page is only show the dropdown when there is
more than one variation. An example of a product with only one is the 15 mm Combo
Wrench (#/product/15mm-combo-wrench). In this instance, it is not worth showing the
box. As we are setting the variation variable on the Product component on
load, we are not relying on the selection to initially set the variable. Because of this, we can
completely remove the select box with a v-if="" when there is only one alternate product.
Like we did with the images, check if the length of the array is more than one, this time the
variationProducts array:
[ 275 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
{{ (variation.quantity) ? 'Add to basket' : 'Out of stock' }}
By removing elements when they are not needed, we now have a less cluttered interface.
Updating the product details when switching
URLs
While navigating through the different product URLs to check variations, you may have
noticed that clicking back and forward doesn't update the product data on the page.
This is because Vue-router realizes the same component is being used between the pages,
and so, rather than destroying and creating a new instance, it reuses the component. The
downside to this is that the data does not get updated; we need to trigger a function to
include the new product data. The upside is that the code is more efficient.
To tell Vue to retrieve the new data, we need to create a watch function; instead of
watching a variable, we are going to watch the $route variable. When this gets updated,
we can load new data.
Create a new variable in the data instance of slug, and set the default to be the route
parameter. Update the product computed function to use this variable instead of the route:
const ProductPage = {
name: 'ProductPage',
template: `...`,
components: {
PageNotFound
},
data() {
return {
slug: this.$route.params.slug,
productNotFound: false,
image: false,
variation: false
}
},
computed: {
[ 276 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.slug];
this.image = (product.images.length) ? product.images[0] : false;
this.variation = product.variationProducts[0];
if(!product) {
this.productNotFound = true;
}
}
console.log(product);
return product;
}
},
watch: {
...
},
methods: {
...
}
};
We can now create a watch function, keeping an eye on the $route variable. When this
changes, we can update the slug variable, which in turn will update the data being
displayed.
When watching a route, the function has two parameters passed to it: to and from. The to
variable contains everything about the route we are going to, including parameters and the
component used. The from variable contains everything about the current route.
By updating the slug variable to the new parameter when the route changes, we are
forcing the component to redraw with new data from the store:
const ProductPage = {
name: 'ProductPage',
template: `
[ 277 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
{{ product.title }} - \${{ variation.price }}
Manufacturer: {{ product.vendor.title }}
Category: {{ product.type }}
Quantity: {{ variation.quantity }}
{{ (variation.quantity) ? 'Add to basket' : 'Out of stock' }}
[ 278 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
`,
components: {
PageNotFound
},
data() {
return {
slug: this.$route.params.slug,
productNotFound: false,
image: false,
variation: false
}
},
computed: {
product() {
let product;
if(Object.keys(this.$store.state.products).length) {
product = this.$store.state.products[this.slug];
this.image = (product.images.length) ? product.images[0] : false;
this.variation = product.variationProducts[0];
if(!product) {
this.productNotFound = true;
}
}
return product;
}
},
watch: {
variation(v) {
if(v.hasOwnProperty('image')) {
this.updateImage(v.image);
}
},
'$route'(to) {
this.slug = to.params.slug;
}
},
[ 279 ]
Using Vue-Router Dynamic Routes to Load Data
Chapter 9
methods: {
updateImage(img) {
this.image = img;
},
variantTitle(variation) {
let variants = variation.variant,
output = [];
for(let a in variants) {
output.push(`${variants[a].name}: ${variants[a].value}`);
}
return output.join(' / ');
},
addToBasket() {
alert(`Added to basket: ${this.product.title} ${this.variantTitle(this.variation)}`);
}
}
};
With our product page completed, we can move on to creating a category listing for both
the type and vendor variables. Remove any console.log() calls you have in your code,
too, to keep it clean.
Summary
This chapter has covered a lot. We loaded and stored a CSV file of products into our Vuex
store. From there, we created a product detail page that used a dynamic variable in the URL
to load a specific product. We have created a product detail view that allows the user to
look through a gallery of images and choose a variation from a drop-down list. If the
variation has an associated image, the main image updates.
In Chapter 10, Building an E-Commerce Store – Browsing Products,
we will create a category page, creating filtering and ordering functions – helping the user
to find the product they want.
[ 280 ]
10
Building an E-Commerce Store
- Browsing Products
In Chapter 9, Using Vue-Router Dynamic Routes to Load Data, we loaded our product data
into the Vuex store and created a product detail page where a user could view the product
and its variations. When viewing the product detail page, a user could change the variation
from the drop-down and the price and other details would update.
In this chapter, we are going to:
Create a home page listing page with specific products
Create a category page with a reusable component
Create an ordering mechanism
Create filters dynamically and allow the user to filter the products
Listing the products
Before we create any filtering, curated lists, ordering components, and functionality, we
need to create a basic product list – showing all the products first, and then we can create a
paginated component that we can then reuse throughout the app.
Building an E-Commerce Store - Browsing Products
Chapter 10
Adding a new route
Let us add a new route to our routes array. For now, we'll work on the HomePage
component, which will have the / route. Make sure you add it to the top of the routes
array, so it doesn't get overridden by any of the other components:
const router = new VueRouter({
routes: [
{
path: '/',
name: 'Home',
component: HomePage
},
{
path: '/product/:slug',
component: ProductPage
},
{
path: '/404',
alias: '*',
component: PageNotFound
}
]
});
Within the HomePage component, create a new computed property and gather all the
products from the store. Ensure the products have loaded before displaying anything in
the template. Populate the HomePage component with the following code:
const HomePage = {
name: 'HomePage',
template: `
`,
computed: {
products() {
return this.$store.state.products;
}
}
};
[ 282 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
Looping through products
When looking at a category listing for any shop, the data displayed tends to have a
recurring theme. It normally consists of an image, title, price, and manufacturer.
Add an ordered list to your template – as the products are going to have an order to them,
it makes semantic sense to place them in an ordered list. Within the , add a v-for
looping through the products and displaying a title for each one, as shown here. It's also
good practice to ensure the product variable exists before proceeding with displaying it:
template: ``,
When viewing the page in your browser, you may notice that the product list is very long.
Loading images for every one of these products would be a huge load on the user's
computer, along with overwhelming the user with that many products on display. Before
we add more information, such as price and images, to our template, we'll look at
paginating the products, allowing the data to be accessed in more manageable chunks.
Creating pagination
Creating pagination, initially, seems quite simple – as you only need to return a fixed
number of products. However, if we wish to make our pagination interactive and reactive
to the product list – it needs to be a bit more advanced. We need to build our pagination to
be able to handle different lengths of products – in the case where our product list has been
filtered into fewer products.
Calculating the values
The arithmetic behind creating a pagination component and displaying the correct
products relies on four main variables:
Items per page: This is usually set by the user; however, we'll use a fixed number
of 12, to begin with
Total items: This is the total number of products to display
[ 283 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
Number of pages: This can be calculated by dividing the number of products by
the items per page
Current page number: This, combined with the others, will allow us to return
exactly which products we need
From these numbers, we can calculate everything needed for our pagination. This includes
what products to display, whether to show next/previous links and, if desired, a
component to skip to different links.
Before we proceed, we are going to convert our products object into an array. This allows
us to use the split method on it, which will allow us to return a specific list of products. It
also means we can easily count the total number of items.
Update your products computed function to return an array instead of an object. This
is done by using the map() function – which is an ES2015 replacement for a simple for
loop. This function now returns an array containing the product objects:
products() {
let products = this.$store.state.products;
return Object.keys(products).map(key => products[key]);
},
Create a new function in the computed object titled pagination. This function will return
an object with various figures about our pagination, for example, the total number of pages.
This will allow us to create a product list and update the navigation components. We need
to only return the object if our products variable has data. The function is shown in the
following code snippet:
computed: {
products() {
let products = this.$store.state.products;
return Object.keys(products).map(key => products[key]);
},
pagination() {
if(this.products) {
return {
}
}
}
},
[ 284 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
We now need to keep track of two variables – items perPage and the currentPage. Create
a data function on your HomePage component and store these two variables. We'll give the
user the ability to update the perPage variable later on. The highlighted code portion
shows our data function:
const HomePage = {
name: 'HomePage',
template: `...`,
data() {
return {
perPage: 12,
currentPage: 1
}
},
computed: {
...
}
};
You may be wondering when to use local data on a component and when
to store the information in the Vuex store. This all depends on where you
are going to be using the data and what is going to manipulating it. As a
general rule, if only one component uses the data and manipulate it, then
use the local data() function. However, if more than one component is
going to be interacting with the variable, save it in the central store.
Back to the pagination() computed function, store a variable with the length of the
products array. With this as a variable, we can now calculate the total pages. To do this,
we are going to do the following equation:
total number of products / items per page
Once we have this result, we need to round it up to the nearest integer. This is because if
there is any hangover, we need to create a new page for it.
[ 285 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
For example, if you were showing 12 items per page and you had 14 products, that would
yield a result of 1.1666 pages – which is not a valid page number. Rounding this up ensures
we have two pages to display our products. To do this, use the Math.ceil() JavaScript
function. We can also add the total number of products to our output. Check the following
code for using the Math.ceil() function:
pagination() {
if(this.products) {
let totalProducts = this.products.length;
return {
totalProducts: totalProducts,
totalPages: Math.ceil(totalProducts / this.perPage)
}
}
}
The next calculation we need to do is work out what the current range of products for the
current page is. This is a little more complicated, as not only do we need to work out what
we need from the page number, but the array slicing is based on the item index – which
means the first item is 0.
To work out where to take our slice from, we can use the following calculation:
(current page number * items per page) – items per page
The final subtraction may seem odd, but it means on page 1, the result is 0. This allows us
to work out at which index we need to slice the products array.
As another example, if we were on page three, the result would be 24, which is where the
third page would start. The end of the slice is then this result plus the number of items per
page. The advantage of this means we can update the items per page and all of our
calculations will update.
Create an object inside the pagination result with these two results – this will allow us to
access them later easily:
pagination() {
if(this.products) {
let totalProducts = this.products.length,
pageFrom = (this.currentPage * this.perPage) - this.perPage;
return {
totalProducts: totalProducts,
totalPages: Math.ceil(totalProducts / this.perPage),
range: {
from: pageFrom,
[ 286 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
to: pageFrom + this.perPage
}
}
}
}
Displaying a paginated list
With our pagination properties calculated, we are now able to manipulate our products
array using the start and end points. Rather than use a hardcoded value, or use another
computed function, we are going to use a method to truncate the product list. This has the
advantage of being able to pass on any list of products while also meaning Vue does not
cache the result.
Create a new method object inside your component with a new method of paginate. This
should accept a parameter that will be the array of products for us to slice. Within the
function, we can use the two variables we calculated previously to return the right number
of products:
methods: {
paginate(list) {
return list.slice(
this.pagination.range.from,
this.pagination.range.to
);
}
}
Update the template to use this method when looping through the products:
template: ``,
We can now view this in our browser and note it returns the first 12 products from our
object. Updating the currentPage variable within the data object to two or three will
reveal different lists of products, depending on the number.
[ 287 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
To continue our semantic approach to listing our products, we should update the start
position of our ordered list when not on page one. This can be done using the HTML
attribute of start – this allows you to specify with which number you should start an
ordered list.
Use the pagination.range.from variable to set the starting point of our ordered list –
remembering to add 1 as on the first page it will be 0:
template: ``
When incrementing the page numbers in the code now, you will notice the ordered list
starts at the appropriate place for each page.
Creating paginating buttons
Updating the page number via the code isn't user-friendly – so we should add some pages
to increment and decrement the page number variable. To do this, we'll create a function
that changes the currentPage variable to its value. This allows us to use it for both
the Next page and Previous page buttons, plus a numbered page list if desired.
Begin by creating two buttons within your pagination container. We want to disable
these buttons if we are at the extremities of the navigations – for example, you don't want to
be able to go below 1 when going back, and past the maximum number of pages when
going forward. We can do this by setting the disabled attribute on the button – like we
did on the product detail page and comparing the current page against these limits.
Add a disabled attribute and, on the Previous page, the button checks if the current page
is one. On the Next page button, compare it to the totalPages value of our pagination
method. The code for implementing the previously mentioned attributes is shown here:
Previous page
Next page
[ 288 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
Set the currentPage variable back to 1 and load the home page up in the browser. You
should notice the Previous page button is disabled. If you change the currentPage
variable, you will notice the buttons become active and inactive as desired.
We now need to create a click method for the buttons to update the currentPage. Create a
new function titled toPage(). This should accept a single variable – this will directly
update the currentPage variable:
methods: {
toPage(page) {
this.currentPage = page;
},
paginate(list) {
return list.slice(this.pagination.range.from,
this.pagination.range.to);
}
}
Add the click handlers to the buttons, passing through currentPage + 1 for the Next
page button, and currentPage - 1 for the Previous page button:
template: `
Previous page
Next page
{{ product.title }}
`
We can now navigate back and forth through the products. As a nice addition to the user
interface, we could give an indication of the page number and how many pages remain,
using the variables available to us by using the code mentioned here:
template: `
Page {{ currentPage }} out of {{ pagination.totalPages }}
Previous page
Next page
[ 289 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
{{ product.title }}
`
Updating the URL on navigation
Another improvement to the user experience would be to update the URL on page
navigation – this would allow the user to share the URL, bookmark it, and return to it later.
When paginating, the pages are a temporary state and should not be the main endpoint of a
URL. Instead, we can take advantage of the query parameters with Vue router.
Update the toPage method to add the parameter to the URL on page change. This can be
achieved using $router.push, however, we need to be careful not to remove any
existing parameters that may be in use for filtering in the future. This can be achieved by
combining the current query object from the route with a new one containing the page
variable:
toPage(page) {
this.$router.push({
query: Object.assign({}, this.$route.query, {
page
})
});
this.currentPage = page;
},
While navigating from page to page, you will notice the URL obtaining a new parameter of
?page= equal to the current page name. However, pressing refresh will not yield the
correct page results but, instead, page one again. This is because we need to pass the
current page query parameter to the currentPage variable on our HomePage component.
[ 290 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
This can be done using the created() function – updating the variables – ensuring we've
checked for its existence first. The created function is part of the Vue life cycle and was
covered in Chapter 4, Getting a List of Files Using the Dropbox API:
created() {
if(this.$route.query.page) {
this.currentPage = parseInt(this.$route.query.page);
}
}
We need to ensure the currentPage variable is an integer, to help us with any arithmetic
we need to do later on as a string is not a fan of calculations.
Creating pagination links
When viewing paginated products, it's often good practice to have a truncated list of page
numbers, allowing the user to jump several pages. We already have the mechanism for
navigating between pages – this can extend that.
As a simple entry point, we can create a link to every page by looping through until we
reach the totalPages value. Vue allows us to do this without any JavaScript. Create a nav
element at the bottom of the component with a list inside. Using a v-for, and create a
variable of page for every item in the totalPages variable:
{{ page }}
This will create a button for every page – for example, if there are 24 pages in total, this will
create 24 links. This is not the desired effect, as we want a few pages before and after the
current page. An example of this would be, if the current page is 15, the page links should
be 12, 13, 14, 15, 16, 17 and 18. This means there are fewer links and it is less overwhelming
for the user.
[ 291 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
To begin with, create a new variable in the data object, which will note how many pages to
show either side of the selected page – a good value to start with is three:
data() {
return {
perPage: 12,
currentPage: 1,
pageLinksCount: 3
}
},
Next, create a new computed function titled pageLinks. This function will need to take the
current page and work out what page numbers are three less and three more than that.
From there, we need to check that the lower range is not less than one, and the upper is not
more than the total number of pages. Check that the products array has items before
proceeding:
pageLinks() {
if(this.products.length) {
let negativePoint = parseInt(this.currentPage) - this.pageLinksCount,
positivePoint = parseInt(this.currentPage) + this.pageLinksCount;
if(negativePoint < 1) {
negativePoint = 1;
}
if(positivePoint > this.pagination.totalPages) {
positivePoint = this.pagination.totalPages;
}
return pages;
}
}
The last step is to create an array and a for loop that loops from the lower range to the
higher range. This will create an array containing, at most, seven numbers with the page
range:
pageLinks() {
if(this.products.length) {
let negativePoint = parseInt(this.currentPage) - this.pageLinksCount,
positivePoint = parseInt(this.currentPage) + this.pageLinksCount,
pages = [];
if(negativePoint < 1) {
[ 292 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
negativePoint = 1;
}
if(positivePoint > this.pagination.totalPages) {
positivePoint = this.pagination.totalPages;
}
for (var i = negativePoint; i <= positivePoint; i++) {
pages.push(i)
}
return pages;
}
}
We can now replace the pagination.totalPages variable in our navigation component
with the new pageLinks variable and the correct number of links will be created, as shown
here:
Viewing this in the browser, however, will render some odd behavior. Although the correct
number of links will be generated, clicking on them or using the next/previous buttons will
cause the buttons to remain the same – even if you navigate out of the range of the buttons.
This is because the computed value is cached. We can combat this in two ways – either
move the function into the method object or, alternatively, add a watch function to watch
the route and update the current page.
Opting for the second option means we can ensure no other results and outputs get cached
and are updated accordingly. Add a watch object to your component and update the
currentPage variable to that of the page query variable. Ensure it exists, otherwise default
to one. The watch method is as shown here:
watch: {
'$route'(to) {
this.currentPage = parseInt(to.query.page) || 1;
}
}
[ 293 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
This ensures all the computed variables update when a different page is navigated to. Open
your HomePage component and ensure all your pagination components work accordingly
and update the list.
Updating the items per page
The last user interface addition we need to create is allowing the user to update the number
of products per page. To initially set this up, we can create a box with a v-model
attribute that updates the value directly. This works as expected and updates the product
list accordingly, as shown:
template: `
Page {{ currentPage }} out of {{ pagination.totalPages }}
Products per page:
12
24
48
60
Previous page
Next page
{{ product.title }}
[ 294 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
The issue with this is if the user is on a page higher than is possible once the value has
changed. For example, if there are 30 products with 12 products per page, this would create
three pages. If the user navigates to page three and then selects 24 products per page, there
would only be two pages needed and page three would be empty.
This can be resolved, once again, with a watch function. When the perPage variable
updates, we can check if the current page is higher than the totalPages variable. If it is,
we can redirect it to the last page:
watch: {
'$route'(to) {
this.currentPage = parseInt(to.query.page);
},
perPage() {
if(this.currentPage > this.pagination.totalPages) {
this.$router.push({
query: Object.assign({}, this.$route.query, {
page: this.pagination.totalPages
})
})
}
}
}
Creating the ListProducts component
Before we proceed with creating the filtering and ordering, we need to extract our product
listing logic and template it into our component – allowing us to easily reuse it. This
component should accept a prop of products, which it should be able to list and paginate.
Open up the ListProducts.js file and copy the code from the HomePage.js file into the
component. Move the data object and copy the pagination and pageLinks computed
functions. Move the watch and methods objects, as well as the created() function, from
the HomePage to the ListProducts file.
Update the HomePage template to use the components with a
products prop, passing in the products computed value. In comparison, the HomePage
component should now be significantly smaller:
const HomePage = {
name: 'HomePage',
[ 295 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
template: `
`,
computed: {
products() {
let products = this.$store.state.products;
return Object.keys(products).map(key => products[key]);
}
}
};
Within the ListProducts component, we need to add a props object, to let the component
know what to expect. This component is now significant. There are a few more things we
need to add to this component to make it more versatile. They include:
Showing the next/previous links if there is more than one page
Showing the "products per page" component if there are more than 12 products,
and only showing each step if there are more products than in the preceding step
Only showing the pageLinks component if it's more than our pageLinksCount
variable
All of these additions have been added to the following component code as follows. We
have also removed the unnecessary products computed value:
Vue.component('list-products', {
template: `
Page {{ currentPage }} out of {{ pagination.totalPages }}
Products per page:
12
24
48
60
[ 296 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
Previous page
Next page
{{ product.title }}
`,
props: {
products: Array
},
data() {
return {
perPage: 12,
currentPage: 1,
pageLinksCount: 3
}
},
computed: {
pagination() {
if(this.products) {
let totalProducts = this.products.length,
pageFrom = (this.currentPage * this.perPage) - this.perPage,
totalPages = Math.ceil(totalProducts / this.perPage);
return {
totalProducts: totalProducts,
totalPages: Math.ceil(totalProducts / this.perPage),
range: {
from: pageFrom,
[ 297 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
to: pageFrom + this.perPage
}
}
}
},
pageLinks() {
if(this.products.length) {
let negativePoint = this.currentPage - this.pageLinksCount,
positivePoint = this.currentPage + this.pageLinksCount,
pages = [];
if(negativePoint < 1) {
negativePoint = 1;
}
if(positivePoint > this.pagination.totalPages) {
positivePoint = this.pagination.totalPages;
}
for (var i = negativePoint; i <= positivePoint; i++) {
pages.push(i)
}
return pages;
}
}
},
watch: {
'$route'(to) {
this.currentPage = parseInt(to.query.page);
},
perPage() {
if(this.currentPage > this.pagination.totalPages) {
this.$router.push({
query: Object.assign({}, this.$route.query, {
page: this.pagination.totalPages
})
})
}
}
},
created() {
if(this.$route.query.page) {
this.currentPage = parseInt(this.$route.query.page);
[ 298 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
}
},
methods: {
toPage(page) {
this.$router.push({
query: Object.assign({}, this.$route.query, {
page
})
});
this.currentPage = page;
},
paginate(list) {
return list.slice(this.pagination.range.from,
this.pagination.range.to)
}
}
});
You can verify your conditional rendering tags are working by temporarily truncating the
products array in the HomePage template – don't forget to remove it once you're done:
products() {
let products = this.$store.state.products;
return Object.keys(products).map(key => products[key]).slice(1, 10);
}
Creating a curated list for the home page
With our product listing component in place, we can proceed with making a curated list of
products for our home page, and add more information to the product listing.
In this example, we are going to hardcode an array of product handles on our home page
component that we want to display. If this were in development, you would expect this list
to be controlled via a content management system or similar.
[ 299 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
Create a data function on your HomePage component, that which includes an array
titled selectedProducts:
data() {
return {
selectedProducts: []
}
},
Populate the array with several handles from the product list. Try and get about six, but if
you go over 12, remember it will paginate with our component. Add your selected handles
to the selectedProducts array:
data() {
return {
selectedProducts: [
'adjustable-stem',
'colorful-fixie-lima',
'fizik-saddle-pak',
'kenda-tube',
'oury-grip-set',
'pure-fix-pedals-with-cages'
]
}
},
With our selected handles, we can now filter the product list to only include a list of
products included in our selectedProducts array. The initial instinct might be to use the
JavaScript filter() function on the products array combined with includes():
products() {
let products = this.$store.state.products;
products = Object.keys(products).map(key => products[key]);
products = products.filter(product =>
this.selectedProducts.includes(product.handle));
return products;
}
The issue with this is that, although it appears to work, it does not respect the ordering of
the selected products. The filter function simply removes any items that do not match and
leaves the remaining products in the order they are loaded.
Fortunately, our products are saved in a key/value pair with the handle as the key. Using
this, we can utilize the products object and return an array using a for loop.
[ 300 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
Create an empty array, output, within the computed function. Looping through
the selectedProducts array, find each required product and add to the output array:
products() {
let products = this.$store.state.products,
output = [];
if(Object.keys(products).length) {
for(let featured of this.selectedProducts) {
output.push(products[featured]);
}
return output;
}
}
This creates the same product list but, this time, in the correct order. Try re-ordering,
adding, and deleting items to ensure your list reacts accordingly.
Showing more information
We can now work on showing more product information in our ListProduct component.
As mentioned near the beginning of the chapter, we should display:
Image
Title
Price
Manufacturer
We're already displaying the title, and the image and manufacturer can easily be pulled out
from the product information. Don't forget to always retrieve the first image from the
images array. Open up the ListProducts.js file and update the product to display this
information – making sure you check whether the image exists before displaying it.
The manufacturer title is listed under the vendor object in the product data:
{{ product.title }}
Made by: {{ product.vendor.title }}
[ 301 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
The price is going to be a little more complicated to work out. This is because each variation
on the product can have a different price, however, these are often the same. If there are
different prices we should display the cheapest one with a from prepended.
We need to create a function that loops through the variations and works out the cheapest
price and, if there is a price range, add the word from. To achieve this, we are going to loop
through the variations and build up an array of unique prices – if the price does not already
exist in the array. Once complete, we can check the length – if there is more than one price,
we can add the prefix, if not, it means all variations are the same price.
Create a new method on the ListProducts component called productPrice. This
accepts one parameter, which will be the variations. Inside, create an empty array, prices:
productPrice(variations) {
let prices = [];
}
Loop through the variations and append the price to the prices array if it does not exist
already. Create a for loop that uses the includes() function to check if the price exists in
the array:
productPrice(variations) {
let prices = [];
for(let variation of variations) {
if(!prices.includes(variation.price)) {
prices.push(variation.price);
}
}
}
With our array of prices, we can now extract the lowest number and check whether there is
more than one item.
To extract the lowest number from an array, we can use the JavaScript Math.min()
function. Use the .length property to check the length of the array. Lastly, return the
price variable:
productPrice(variations) {
let prices = [];
for(let variation of variations) {
if(!prices.includes(variation.price)) {
prices.push(variation.price);
}
}
[ 302 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
let price = '$' + Math.min(...prices);
if(prices.length > 1) {
price = 'From: ' + price;
}
return price;
}
Add your productPrice method to your template, remembering to
pass product.variationProducts into it. The last thing we need to add to our template
is a link to the product:
{{ product.title }}
Made by: {{ product.vendor.title }}
Price {{ productPrice(product.variationProducts) }}
Ideally, the product links should use a named route and not a hardcoded link, in case the
route changes. Add a name to the product route and update the to attribute to use the
name instead:
{
path: '/product/:slug',
name: 'Product',
component: ProductPage
}
[ 303 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
Update the template to now use the route name, with the params object:
{{ product.title }}
Made by: {{ product.vendor.title }}
Price {{ productPrice(product.variationProducts) }}
Creating categories
A shop is not really a usable shop if it does not have categories to navigate by. Fortunately,
each of our products has a type key that indicates a category for it to be shown in. We can
now create a category page that lists products from that particular category.
Creating a category list
Before we can display the products in a particular category, we first need to generate a list
of available categories. To help with the performance of our app, we will also store the
handles of the products in each category. The categories structure will look like the
following:
categories = {
tools: {
name: 'Tools',
handle: 'tools',
products: ['product-handle', 'product-handle'...]
},
freewheels: {
name: 'Freewheels',
handle: 'freewheels',
products: ['another-product-handle', 'product'...]
}
};
[ 304 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
Creating the category list like this means we readily have available the list of products
within the category while being able to loop through the categories and output the title
and handle to create a list of links to categories. As we already have this information, we
will create the category list once we've retrieved the product list.
Open up app.js and navigate to the created() method on the Vue instance. Rather than
creating a second $store.commit underneath the products storing method, we are going
to utilize a different functionality of Vuex – actions.
Actions allow you to create functions in the store itself. Actions are unable to mutate the
state directly – that is still down to mutations, but it allows you to group several mutations
together, which in this instance, suits us perfectly. Actions are also perfect if you want to
run an asynchronous operation before mutating the state – for example with a setTimeout
JavaScript function.
Navigate to your Vuex.Store instance and, after the mutations, add a new object of
actions. Inside, create a new function titled initializeShop:
const store = new Vuex.Store({
state: {
products: {}
},
mutations: {
products(state, payload) {
state.products = payload;
}
},
actions: {
initializeShop() {
}
}
});
With action parameters, the first parameter is the store itself, which we need to use in order
to utilize the mutations. There are two ways of doing this, the first is to use a single variable
and access its properties within the function. For example:
actions: {
initializeShop(store) {
store.commit('products');
}
}
[ 305 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
However, with ES2015, we are able to use argument destructuring and utilize the
properties we need. For this action, we only need the commit function, like so:
actions: {
initializeShop({commit}) {
commit('products');
}
}
If we wanted the state from the store as well, we could add it to the curly brackets:
actions: {
initializeShop({state, commit}) {
commit('products');
// state.products
}
}
Using this "exploded" method of accessing the properties makes our code cleaner and less
repetitive. Remove the state property and add a second parameter after the curly brackets
labeled products. This will be our formatted product's data. Pass that variable directly to
the product's commit function:
initializeShop({commit}, products) {
commit('products', products);
}
Using actions is as simple as using mutations, except instead of using $store.commit,
you use $store.dispatch. Update your created method – not forgetting to change the
function name too, and check your app still works:
created() {
CSV.fetch({url: './data/csv-files/bicycles.csv'}).then(data => {
this.$store.dispatch('initializeShop', this.$formatProducts(data));
});
}
The next step is to create a mutation for our categories. As we may want to update our
categories independently of our products – we should create a second function within the
mutations. It should also be this function that loops through the products and creates the
list of categories.
[ 306 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
First, make a new property in the state object titled categories. This should be an object
by default:
state: {
products: {},
categories: {}
}
Next, create a new mutation called categories. Along with the state, this should take a
second parameter. To be consistent, title it payload – as this is what Vuex refers to it as:
mutations: {
products(state, payload) {
state.products = payload;
},
categories(state, payload) {
}
},
Now for the functionality. This mutation needs to loop through the products. For every
product, it needs to isolate the type. Once it has the title and slug, it needs to check if an
entry exists with that slug; if it does, append the product handle to the products array, if
not – it needs to create a new array and details.
Create an empty categories object and loop through the payload, setting a variable for
both the product and type:
categories(state, payload) {
let categories = {};
Object.keys(payload).forEach(key => {
let product = payload[key],
type = product.type;
});
}
We now need to check if an entry exists with the key of the current type.handle. If it does
not, we need to create a new entry with it. The entry needs to have the title, handle, and an
empty products array:
categories(state, payload) {
let categories = {};
Object.keys(payload).forEach(key => {
let product = payload[key],
type = product.type;
[ 307 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
if(!categories.hasOwnProperty(type.handle)) {
categories[type.handle] = {
title: type.title,
handle: type.handle,
products: []
}
}
});
}
Lastly, we need to append the current product handle onto the products array of the entry:
categories(state, payload) {
let categories = {};
Object.keys(payload).forEach(key => {
let product = payload[key],
type = product.type;
if(!categories.hasOwnProperty(type.handle)) {
categories[type.handle] = {
title: type.title,
handle: type.handle,
products: []
}
}
categories[type.handle].products.push(product.handle);
});
}
You can view the categories output by adding console.log to the end of the function:
categories(state, payload) {
let categories = {};
Object.keys(payload).forEach(key => {
...
});
console.log(categories);
}
[ 308 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
Add the mutation to the initializeShop action:
initializeShop({commit}, products) {
commit('products', products);
commit('categories', products);
}
Viewing the app in the browser, you will be faced with a JavaScript error. This is because
some products do not contain a "type" for us to use to categorize them. Even with the
JavaScript error resolved, there are still a lot of categories that get listed out.
To help with the number of categories, and to group the uncategorized products, we
should make an "Miscellaneous" category. This will collate all the categories with two or
fewer products and group the products into their own group.
Creating a "miscellaneous" category
The first issue we need to negate is the nameless category. When looping through our
products, if no type is found, we should insert a category, so everything is categorized.
Create a new object in the categories method that contains the title and handle for a new
category. For the handle and variable call it other. Make the title a bit more user-friendly by
calling it Miscellaneous.
let categories = {},
other = {
title: 'Miscellaneous',
handle: 'other'
};
When looping through products, we can then check to see whether the type key exists, if
not, create an other category and append to it:
Object.keys(payload).forEach(key => {
let product = payload[key],
type = product.hasOwnProperty('type') ? product.type : other;
if(!categories.hasOwnProperty(type.handle)) {
categories[type.handle] = {
title: type.title,
handle: type.handle,
products: []
}
}
[ 309 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
categories[type.handle].products.push(product.handle);
});
Viewing the app now will reveal all the categories in the JavaScript console – allowing you
to see the magnitude of how many categories there are.
Let's combine any categories with two or fewer products into the "other" category – not
forgetting to remove the category afterward. After the product loop, loop through the
categories, checking the count of the products available. If fewer than three, add them to
the "other" category:
Object.keys(categories).forEach(key => {
let category = categories[key];
if(category.products.length < 3) {
categories.other.products =
categories.other.products.concat(category.products);
}
});
We can then delete the category we've just stolen the products from:
Object.keys(categories).forEach(key => {
let category = categories[key];
if(category.products.length < 3) {
categories.other.products =
categories.other.products.concat(category.products);
delete categories[key];
}
});
And with that, we have a much more manageable list of categories. One more
improvement we can make is to ensure the categories are in alphabetical order. This helps
users find their desired category much quicker. In JavaScript, arrays can be sorted a lot
more easily than objects, so once again, we need to loop through an array of the object keys
and sort them. Create a new object and add the categories as they are sorted to it.
Afterward, store this on the state object so we have the categories available to us:
categories(state, payload) {
let categories = {},
other = {
title: 'Miscellaneous',
handle: 'other'
};
Object.keys(payload).forEach(key => {
[ 310 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
let product = payload[key],
type = product.hasOwnProperty('type') ? product.type : other;
if(!categories.hasOwnProperty(type.handle)) {
categories[type.handle] = {
title: type.title,
handle: type.handle,
products: []
}
}
categories[type.handle].products.push(product.handle);
});
Object.keys(categories).forEach(key => {
let category = categories[key];
if(category.products.length < 3) {
categories.other.products =
categories.other.products.concat(category.products);
delete categories[key];
}
});
let categoriesSorted = {}
Object.keys(categories).sort().forEach(key => {
categoriesSorted[key] = categories[key]
});
state.categories = categoriesSorted;
}
With that, we can now add a list of categories to our HomePage template. For this, we'll
create named router-view components – allowing us to put things in the sidebar of the
shop on selected pages.
Displaying the categories
With our categories stored, we can now proceed with creating our ListCategories
component. We want to display our category navigation in a sidebar on the home page,
and also on a shop category page. As we want to show it in several places, we have a
couple of options as to how we display it.
[ 311 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
We can use the component in the template as we have with the component. The issue with this is that if we want to display our list in a sidebar
and our sidebar needs to be consistent across the site, we would have to copy and paste a
lot of HTML between views.
A better approach would be to use named routes and set the template once in our
index.html.
Update the app template to contain a and an element. Within these, create
a router-view, leaving the one inside main unnamed, while giving the one inside the
aside element a name of sidebar:
Within our routes object, we can now add different components to different named views.
On the Home route, change the component key to components, and add an object specifying each component and its view:
{
path: '/',
name: 'Home',
components: {
default: HomePage,
sidebar: ListCategories
}
}
The default indicates that the component will go into the unnamed router-view. This
allows us to still use the singular component key if required. For the component to be
correctly loaded into the sidebar view, we need to alter how the ListCategories
component is initialized. Instead of using Vue.component, initialize it as you would a
view component:
const ListCategories = {
name: 'ListCategories'
};
[ 312 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
We can now proceed with making the template for the category list. As our categories are
saved in the store, loading and displaying them should be familiar by now. It is advised
you load the categories from the state into a computed function - for cleaner template code
and easier adaptation should you need to manipulate it in any way.
Before we create the template, we need to create a route for the category. Referring back to
our plan in Chapter 9, Using Vue-Router Dynamic Routes to Load Data, we can see the route
is going to be /category/:slug – add this route with a name and enable props, as we'll
utilize them for the slug. Ensure you have made the CategoryPage file and initialized the
component.
const router = new VueRouter({
routes: [
{
path: '/',
name: 'Home',
components: {
default: HomePage,
sidebar: ListCategories
}
},
{
path: '/category/:slug',
name: 'Category',
component: CategoryPage,
props: true
},
{
path: '/product/:slug',
name: 'Product',
component: ProductPage
},
{
path: '/404',
alias: '*',
component: PageNotFound
}
]
});
[ 313 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
Back to our ListCategories component; loop through the stored categories and create a
link for each one. Show the product count in brackets after each name:
const ListCategories = {
name: 'ListCategories',
template: `
{{ category.title }} ({{ category.products.length }})
`,
computed: {
categories() {
return this.$store.state.categories;
}
}
};
With the links to our categories now showing on the home page, we can head on to make a
category page.
Displaying products in a category
Clicking one of the category links (that is, /#/category/grips) will navigate to a blank
page – thanks to our route. We need to create a template and set up the category page to
show the products. As a starting base, create the CategoryPage component in a similar
vein to the product page.
Create a template with an empty container and the PageNotFound component inside.
Create a data variable titled categoryNotFound, and ensure the PageNotFound
component displays if this is set to true. Create a props object, which allows the slug
property to be passed and, lastly, create a category computed function.
The CategoryPage component should look like the following:
const CategoryPage = {
name: 'CategoryPage',
[ 314 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
template: ``,
components: {
PageNotFound
},
props: {
slug: String
},
data() {
return {
categoryNotFound: false,
}
},
computed: {
category() {
}
}
};
Inside the category computed function, load the correct category from the store based on
the slug. If it is not on the list, mark the categoryNotFound variable to true - similar to
what we did in the ProductPage component:
computed: {
category() {
let category;
if(Object.keys(this.$store.state.categories).length) {
category = this.$store.state.categories[this.slug];
if(!category) {
this.categoryNotFound = true;
}
}
return category;
}
}
[ 315 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
With our category loaded, we can output the title in the template:
template: ``,
We can now proceed with displaying the products on our category page. To do this, we can
use the code from the HomePage component as we have exactly the same scenario – an
array of product handles.
Create a new computed function that takes the current category products and processes
them as we did on the home page:
computed: {
category() {
...
},
products() {
if(this.category) {
let products = this.$store.state.products,
output = [];
for(let featured of this.category.products) {
output.push(products[featured]);
}
return output;
}
}
}
We don't need to check whether the products exist in this function as we are checking
whether the category exists, and that would only return true if the data had been loaded.
Add the component to the HTML and pass in the products variable:
template: ``
[ 316 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
With that, we have our category products listed out for each category.
Code optimization
With our CategoryPage component complete, we can see a lot of similarities between that
and the home page – the only difference being the home page has a fixed product array. To
save repetition, we can combine these two components – meaning we only have to ever
update one if we need to.
We can address the fixed array issue by displaying it when we identify that we are on the
home page. The way of doing that is to check if the slug prop has a value. If not, we can
assume we are on the home page.
First, update the Home route to point to the CategoryPage component and enable props.
When using named views, you have to enable props for each of the views. Update the
props value to be an object with each of the named views, enabling the props for each:
{
path: '/',
name: 'Home',
components: {
default: CategoryPage,
sidebar: ListCategories
},
props: {
default: true,
sidebar: true
}
}
Next, create a new variable in the data function of the CategoryPage, titled
categoryHome. This is going to be an object that follows the same structure as the category
objects, containing a products array, title, and handle. Although the handle won't be used,
it is good practice to follow conventions:
data() {
return {
categoryNotFound: false,
categoryHome: {
title: 'Welcome to the Shop',
handle: 'home',
products: [
'adjustable-stem',
'fizik-saddle-pak',
[ 317 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
'kenda-tube',
'colorful-fixie-lima',
'oury-grip-set',
'pure-fix-pedals-with-cages'
]
}
}
}
The last thing we need to do is check whether the slug exists. If not, assign our new object
to the category variable within the computed function:
category() {
let category;
if(Object.keys(this.$store.state.categories).length) {
if(this.slug) {
category = this.$store.state.categories[this.slug];
} else {
category = this.categoryHome;
}
if(!category) {
this.categoryNotFound = true;
}
}
return category;
}
Head to the home page and verify your new component is working. If it is, you can delete
HomePage.js and remove it from index.html. Update the category route to also include
the category list in the sidebar and use the props object:
{
path: '/category/:slug',
name: 'Category',
components: {
default: CategoryPage,
sidebar: ListCategories
},
props: {
default: true,
sidebar: true
}
},
[ 318 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
Ordering products in a category
With our category pages displaying the right products, it's time to add some ordering
options within our ListProducts component. When viewing a shop online, you can
normally order the products by the following:
Title: Ascending (A - Z)
Title: Descending (Z - A)
Price: Ascending ($1 - $999)
Price: Descending ($999 - $1)
However, once we have the mechanism in place, you can add any ordering criteria you
want.
Start off by creating a select box in your ListProducts component with each of the
preceding values. Add an extra first one of Sort products by...:
Order products
Title - ascending (A - Z)
Title - descending (Z - A)
Price - ascending ($1 - $999)
Price - descending ($999 - $1)
We now need to create a variable for the select box to update in the data function. Add a
new key titled ordering and add a value to each option, so interpreting the value is easier.
Construct the value by using the field and order, separated by a hyphen. For
example, Title - ascending (A - Z) would become title-asc:
Order products
Title - ascending (A - Z)
Title - descending (Z - A)
Price - ascending ($1 - $999)
Price - descending ($999 - $1)
[ 319 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
And the updated data function becomes:
data() {
return {
perPage: 12,
currentPage: 1,
pageLinksCount: 3,
ordering: ''
}
}
To update the order of the products we now need to manipulate the product list. This
needs to be done before the list gets split for pagination - as the user would expect the
whole list to be sorted, not just the current page.
Store the product price
Before we proceed, there is an issue we need to address. To sort by price, the price needs to
ideally be available on the product itself, not calculated specifically for the template, which
it currently is. To combat this, we are going to calculate the price before the products get
added to the store. This means it will be available as a property on the product itself, rather
than being dynamically created.
The details we need to know are the cheapest price and whether the product has many
prices within its variations. The latter means we know whether we need to display the
"From:" when listing the products out. We will create two new properties for each
product: price and hasManyPrices.
Navigate to the products mutation in the store and create a new object and a loop of the
products:
products(state, payload) {
let products = {};
Object.keys(payload).forEach(key => {
let product = payload[key];
products[key] = product;
});
state.products = payload;
}
[ 320 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
Copy the code from the productPrice method on the ListProducts component and
place it within the loop. Update the second for loop so it loops
through product.variationProducts. Once this for loop has completed, we can add
the new properties to the product. Lastly, update the state with the new products object:
products(state, payload) {
let products = {};
Object.keys(payload).forEach(key => {
let product = payload[key];
let prices = [];
for(let variation of product.variationProducts) {
if(!prices.includes(variation.price)) {
prices.push(variation.price);
}
}
product.price = Math.min(...prices);
product.hasManyPrices = prices.length > 1;
products[key] = product;
});
state.products = products;
}
We can now update the productPrice method on the ListProducts component. Update
the function so it accepts the product, instead of variations. Remove the for loop from the
function, and update the variables so they use the price and hasManyPrices properties of
the product instead:
productPrice(product) {
let price = '$' + product.price;
if(product.hasManyPrices) {
price = 'From: ' + price;
}
return price;
}
Update the template so the product is passed to the function:
Price {{ productPrice(product) }}
[ 321 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
Wiring up the ordering
With our price readily available, we can proceed with wiring up the ordering. Create a new
computed function titled orderProducts that returns this.products. We want to
ensure we are always sorting from the source and not ordering something that has
previously been ordered. Call this new function from within the paginate function and
remove the parameter from this method and from the template:
computed: {
...
orderProducts() {
return this.products;
},
},
methods: {
paginate() {
return this.orderProducts.slice(
this.pagination.range.from,
this.pagination.range.to
);
},
}
To determine how we need to sort the products, we can use the this.ordering value. If it
exists, we can split the string on the hyphen, meaning we have an array containing the field
and order type. If it does not exist, we need to simply return the existing product array:
orderProducts() {
let output;
if(this.ordering.length) {
let orders = this.ordering.split('-');
} else {
output = this.products;
}
return output;
}
Sort the products array based on the value of the first item of the ordering array. If it is a
string, we will use localCompare, which ignores cases when comparing. Otherwise, we
will simply subtract one value from the other – this is what the sort function expects:
orderProducts() {
let output;
[ 322 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
if(this.ordering.length) {
let orders = this.ordering.split('-');
output = this.products.sort(function(a, b) {
if(typeof a[orders[0]] == 'string') {
return a[orders[0]].localeCompare(b[orders[0]]);
} else {
return a[orders[0]] - b[orders[0]];
}
});
} else {
output = this.products;
}
return output;
}
Lastly, we need to check if the second item in the orders array is asc or desc. By default,
the current sort function will return the items sorted in an ascending order, so if the value
is desc, we can reverse the array:
orderProducts() {
let output;
if(this.ordering.length) {
let orders = this.ordering.split('-');
output = this.products.sort(function(a, b) {
if(typeof a[orders[0]] == 'string') {
return a[orders[0]].localeCompare(b[orders[0]]);
} else {
return a[orders[0]] - b[orders[0]];
}
});
if(orders[1] == 'desc') {
output.reverse();
}
} else {
output = this.products;
}
return output;
}
Head to your browser and check out the ordering of products!
[ 323 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
Creating Vuex getters
The last step to making our category page just like any other shop is the introduction of
filtering. Filtering allows you to find products that have particular sizes, colors, tags, or
manufacturers. Our filtering options are going to be built from the products on the page.
For example, if none of the products have an XL size or a blue color, there is no point
showing that as a filter.
To achieve this, we are going to need to pass the products of the current category to the
filtering component as well. However, the products get processed on the CategoryPage
component. Instead of repeating this processing, we can move the functionality to a Vuex
store getter. Getters allow you to retrieve data from the store and manipulate it like you
would in a function on a component. Because it is a central place, however, it means several
components can benefit from the processing.
Getters are the Vuex equivalent of computed functions. They are declared as functions but
called as variables. However, they can be manipulated to accept parameters by returning a
function inside them.
We are going to move both the category and products functions from the
CategoryPage component into the getter. The getter function will then return an object
with the category and products.
Create a new object in your store titled getters. Inside, create a new function called
categoryProducts:
getters: {
categoryProducts: () => {
}
}
Getters themselves receive two parameters, the state as the first, and any other getters as
the second. To pass a parameter to a getter, you have to return a function inside of the
getter that receives the parameter. Fortunately, in ES2015, this can be achieved with the
double arrow (=>) syntax. As we are not going to be using any other getters in this function,
we do not need to call the second parameter.
[ 324 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
As we are abstracting all of the logic out, pass in the slug variable as the parameter of the
second function:
categoryProducts: (state) => (slug) => {
}
As we are transferring the logic for selecting and retrieving the categories and products into
the store, it makes sense to store the HomePage category content in the state itself:
state: {
products: {},
categories: {},
categoryHome: {
title: 'Welcome to the Shop',
handle: 'home',
products: [
'adjustable-stem',
'fizik-saddle-pak',
'kenda-tube',
'colorful-fixie-lima',
'oury-grip-set',
'pure-fix-pedals-with-cages'
]
}
}
Move category-selecting logic from the category computed function in the CategoryPage
component into the getter. Update the slug and categoryHome variables to use the
content from the relevant places:
categoryProducts: (state) => (slug) => {
if(Object.keys(state.categories).length) {
let category = false;
if(slug) {
category = this.$store.state.categories[this.slug];
} else {
category = state.categoryHome;
}
}
}
[ 325 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
With a category assigned, we can now load the products based on the handles stored in the
category. Move the code from the products computed function into the getter. Combine
the variable assignments together and remove the store product retrieval variable, as we
have the state readily available. Ensure the code that checks to see whether the category
exists is still in place:
categoryProducts: (state) => (slug) => {
if(Object.keys(state.categories).length) {
let category = false,
products = [];
if(slug) {
category = this.$store.state.categories[this.slug];
} else {
category = state.categoryHome;
}
if(category) {
for(let featured of category.products) {
products.push(state.products[featured]);
}
}
}
}
Lastly, we can add a new productDetails array on the category with the fleshed-out
product data. Return the category at the end of the function. If the slug variable input
exists as a category, we will get all of the data back. If not, it will return false – from
which we can display our PageNotFound component:
categoryProducts: (state) => (slug) => {
if(Object.keys(state.categories).length) {
let category = false,
products = [];
if(slug) {
category = state.categories[slug];
} else {
category = state.categoryHome;
}
if(category) {
for(let featured of category.products) {
products.push(state.products[featured]);
}
category.productDetails = products;
[ 326 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
}
return category;
}
}
In our CategoryPage component, we can remove the products() computed function and
update the category() function. To call a getter function, you refer to
this.$store.getters:
computed: {
category() {
if(Object.keys(this.$store.state.categories).length) {
let category = this.$store.getters.categoryProducts(this.slug);
if(!category) {
this.categoryNotFound = true;
}
return category;
}
}
}
Unfortunately, we are still having to check whether the categories exist before proceeding.
This is so we can tell that there is no category with the name, rather than an unloaded one.
To make this neater, we can extract this check into another getter and utilize it in both our
other getter and the component.
Create a new getter titled categoriesExist, and return the contents of the if statement:
categoriesExist: (state) => {
return Object.keys(state.categories).length;
},
Update the categoryProducts getter to accept getters in the arguments of the first
function and to use this new getter to indicate its output:
categoryProducts: (state, getters) => (slug) => {
if(getters.categoriesExist) {
...
}
}
[ 327 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
In our CategoryPage component, we can now call on the new getter with
this.$store.getters.categoriesExist(). To save having this.$store.getters
repeated twice in this function, we can map the getters to be locally accessed. This allows us
to call this.categoriesExist() as a more readable function name.
At the beginning of the computed object, add a new function
titled ...Vuex.mapGetters(). This function accepts an array or an object as a parameter
and the three dots at the beginning ensure the contents are expanded to be merged with the
computed object.
Pass in an array containing the names of the two getters:
computed: {
...Vuex.mapGetters([
'categoryProducts',
'categoriesExist'
]),
category() {
...
}
}
This now means we have this.categoriesExist and this.categoryProducts at our
disposal. Update the category function to use these new functions:
computed: {
...Vuex.mapGetters([
'categoriesExist',
'categoryProducts'
]),
category() {
if(this.categoriesExist) {
let category = this.categoryProducts(this.slug);
if(!category) {
this.categoryNotFound = true;
}
return category;
}
}
}
[ 328 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
Update the template to reflect the changes in the computed data:
template: ``,
Building the filtering component based on
products
As mentioned, all our filters are going to be created from the products in the current
category. This means if there are no products made by IceToolz, it won't appear as an
available filter.
To begin with, open the ProductFiltering.js component file. Our product filtering is
going to go in our sidebar, so change the component definition from Vue.component to an
object. We still want our categories to display after the filtering, so add the
ListCategories component as a declared component within ProductFiltering. Add a
template key and include the component:
const ProductFiltering = {
name: 'ProductFiltering',
template: `
`,
components: {
ListCategories
}
}
Update the category route to include the ProductFiltering component in the sidebar
instead of ListCategories:
{
path: '/category/:slug',
name: 'Category',
components: {
default: CategoryPage,
[ 329 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
sidebar: ProductFiltering
},
props: {
default: true,
sidebar: true
}
}
You should now have the Home route, which includes the CategoryPage
and ListCategories components, and the Category route, which includes the
ProductFiltering component instead.
From the CategoryPage component, copy the props and computed objects - as we are
going to be utilizing a lot of the existing code. Rename the category computed function to
filters. Remove both the return and the componentNotFound if statement. Your
component should now look like the following:
const ProductFiltering = {
name: 'ProductFiltering',
template: `
`,
components: {
ListCategories
},
props: {
slug: String
},
computed: {
...Vuex.mapGetters([
'categoriesExist',
'categoryProducts'
]),
filters() {
if(this.categoriesExist) {
let category = this.categoryProducts(this.slug);
}
}
}
}
[ 330 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
We now need to construct our filters, based on the products in the category. We will be
doing this by looping through the products, collecting information from preselected values,
and displaying them.
Create a data object that contains a key of topics. This will be an object containing child
objects with a, now familiar, pattern of 'handle': {} for each of the properties we want
to filter on.
Each child object will contain a handle, which is the value of the product of which to filter
(for example, vendor), a title, which is the user-friendly version of the key, and an array
of values, which will be populated.
We'll start off with two, vendor and tags; however, more will be dynamically added as we
process the products:
data() {
return {
topics: {
vendor: {
title: 'Manufacturer',
handle: 'vendor',
values: {}
},
tags: {
title: 'Tags',
handle: 'tags',
values: {}
}
}
}
},
We will now begin looping through the products. Along with the values, we are going to
keep track of how many products have the same value, allowing us to indicate to the user
how many products will be revealed.
Loop through the products on the category within the filters method and, to begin
with, find the vendor of each product. For every one encountered, check whether it exists
within the values array.
[ 331 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
If it does not, add a new object with the name, handle, and a count, which is an array of
product handles. We store an array of handles so that we can verify that the product has
already been seen. If we were keeping a raw numerical count, we could encounter a
scenario where the filters get triggered twice, doubling the count. By checking whether the
product handle exists already, we can check it's only been seen once.
If a filter of that name does exist, add the handle to the array after checking it does not exist:
filters() {
if(this.categoriesExist) {
let category = this.categoryProducts(this.slug),
vendors = this.topics.vendor;
for(let product of category.productDetails) {
if(product.hasOwnProperty('vendor')) {
let vendor = product.vendor;
if(vendor.handle) {
if(!vendor.handle.count.includes(product.handle)) {
category.values[item.handle].count.push(product.handle);
}
} else {
vendors.values[vendor.handle] = {
...vendor,
count: [product.handle]
}
}
}
}
}
}
}
This utilizes the previously-used object-expanding ellipsis (...), which saves us from
having to write:
vendors.values[product.vendor.handle] = {
title: vendor.title,
handle: vendor.handle,
count: [product.handle]
}
[ 332 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
Although, feel free to use this if you are more comfortable with it.
Duplicate the code to work with tags, however as tags are an array themselves, we need
to loop through each tag and add accordingly:
for(let product of category.productDetails) {
if(product.hasOwnProperty('vendor')) {
let vendor = product.vendor;
if(vendor.handle) {
if(!vendor.handle.count.includes(product.handle)) {
category.values[item.handle].count.push(product.handle);
}
} else {
vendors.values[vendor.handle] = {
...vendor,
count: [product.handle]
}
}
}
if(product.hasOwnProperty('tags')) {
for(let tag of product.tags) {
if(tag.handle) {
if(topicTags.values[tag.handle]) {
if(!topicTags.values[tag.handle].count.includes(product.handle))
{
topicTags.values[tag.handle].count.push(product.handle);
}
} else {
topicTags.values[tag.handle] = {
...tag,
count: [product.handle]
}
}
}
}
}
}
Our code is already getting repetitive and complex, let's simplify it by creating a method to
handle the repetitive code.
[ 333 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
Create a methods object with a function of addTopic. This will take two parameters: the
object to append to and the singular item. For example, its usage would be:
if(product.hasOwnProperty('vendor')) {
this.addTopic(this.topics.vendor, product.vendor, product.handle);
}
Create the function and abstract out the logic from inside the hasOwnProperty if
declaration. Name the two parameters category and item, and update the code
accordingly:
methods: {
addTopic(category, item, handle) {
if(item.handle) {
if(category.values[item.handle]) {
if(!category.values[item.handle].count.includes(handle)) {
category.values[item.handle].count.push(handle);
}
} else {
category.values[item.handle] = {
...item,
count: [handle]
}
}
}
}
}
Update the filters computed function to use the new addTopic method. Remove the
variable declarations at the top of the function, as they are being passed directly into the
method:
filters() {
if(this.categoriesExist) {
let category = this.categoryProducts(this.slug);
for(let product of category.productDetails) {
if(product.hasOwnProperty('vendor')) {
this.addTopic(this.topics.vendor, product.vendor, product.handle);
}
if(product.hasOwnProperty('tags')) {
[ 334 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
for(let tag of product.tags) {
this.addTopic(this.topics.tags, tag, product.handle);
}
}
}
}
}
At the end of this function, return this.topics. Although we could reference topics
directly in the template, we need to ensure the filters computed property gets triggered:
filters() {
if(this.categoriesExist) {
...
}
return this.topics;
}
Before we proceed to create our dynamic filters based on the various types, let's display the
current filters.
Due to how the topics object is set up, we can loop through each of the child objects and
then through the values of each one. We are going to make our filters out of checkboxes,
with the value of the input being the handle of each of the filters:
template: ``,
In order to keep track of what is checked, we can use a v-model attribute. If there are
checkboxes with the same v-model, Vue creates an array with each item.
[ 335 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
Add an array of checked to each of the topic objects in the data object:
data() {
return {
topics: {
vendor: {
title: 'Manufacturer',
handle: 'vendor',
checked: [],
values: {}
},
tags: {
title: 'Tags',
handle: 'tags',
checked: [],
values: {}
}
}
}
}
Next, add a v-model attribute to each checkbox, referencing this array on the filter
object along with a click binder, referencing an updateFilters method:
Create an empty method for now - we'll configure it later:
methods: {
addTopic(category, item) {
...
},
updateFilters() {
}
}
[ 336 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
Dynamically creating filters
With our fixed filters created and being watched, we can take the opportunity to create
dynamic filters. These filters will observe the variationTypes on the products (for
example, color, and size) and list out the options – again with the count of each one.
To achieve this, we need to first loop through the variationTypes on the products. Before
adding anything, we need to check to see if that variation type exists on the topics object,
if not – we need to add a skeleton object. This expands the variation (which contains the
title and handle) and also includes empty checked and value properties:
filters() {
if(this.categoriesExist) {
let category = this.categoryProducts(this.slug);
for(let product of category.productDetails) {
if(product.hasOwnProperty('vendor')) {
this.addTopic(this.topics.vendor, product.vendor);
}
if(product.hasOwnProperty('tags')) {
for(let tag of product.tags) {
this.addTopic(this.topics.tags, tag);
}
}
Object.keys(product.variationTypes).forEach(vkey => {
let variation = product.variationTypes[vkey];
if(!this.topics.hasOwnProperty(variation.handle)) {
this.topics[variation.handle] = {
...variation,
checked: [],
values: {}
}
}
});
}
}
return this.topics;
}
[ 337 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
With our empty object created, we can now loop through the variationProducts on the
product object. For each one, we can access the variant with the handle of the current
variation. From there, we can use our addTopic method to include the value (for example,
Blue or XL) within the filters:
Object.keys(product.variationTypes).forEach(vkey => {
let variation = product.variationTypes[vkey];
if(!this.topics.hasOwnProperty(variation.handle)) {
this.topics[variation.handle] = {
...variation,
checked: [],
values: {}
}
}
Object.keys(product.variationProducts).forEach(pkey => {
let variationProduct = product.variationProducts[pkey];
this.addTopic(
this.topics[variation.handle],
variationProduct.variant[variation.handle],
product.handle
);
});
});
We do need to update our addTopic method, however. This is because the dynamic
properties have a value, instead of a title.
Add an if statement to your addTopic method to check whether a value exists, if it does
– set it to the title:
addTopic(category, item, handle) {
if(item.handle) {
if(category.values[item.handle]) {
if(!category.values[item.handle].count.includes(handle)) {
category.values[item.handle].count.push(handle);
}
} else {
if(item.hasOwnProperty('value')) {
item.title = item.value;
}
[ 338 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
category.values[item.handle] = {
...item,
count: [handle]
}
}
}
}
Viewing the app in the browser should reveal your dynamically-added filters, along with
the original ones we had added.
Resetting filters
When navigating between categories you will notice that, currently, the filters do not reset.
This is because we are not clearing the filters between each navigation, and the arrays are
persisting. This is not ideal, as it means they get longer as you navigate around and do not
apply to the products listed.
To remedy this, we can create a method that returns our default topic object and, when the
slug updates, call the method to reset the topics object. Move the topics object to a new
method titled defaultTopics:
methods: {
defaultTopics() {
return {
vendor: {
title: 'Manufacturer',
handle: 'vendor',
checked: [],
values: {}
},
tags: {
title: 'Tags',
handle: 'tags',
checked: [],
values: {}
}
}
},
addTopic(category, item) {
...
}
updateFilters() {
[ 339 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
}
}
Within the data function, change the value of topics to be this.defaultTopics() to call
the method:
data() {
return {
topics: this.defaultTopics()
}
},
Lastly, add a watch function to reset the topics key when the slug gets updated:
watch: {
slug() {
this.topics = this.defaultTopics();
}
}
Updating the URL on checkbox filter change
Our filtering component, when interacted with, is going to update the URL query
parameters. This allows the user to see the filters in effect, bookmark them, and share the
URL if needed. We already used query parameters for our pagination, and it makes sense
to put the user back on page one when filtering – as there may only be one page.
To construct our query parameters of filters, we need to loop through each filter type and
add a new parameter for each one that has items in the checked array. We can then call a
router.push() to update the URL and, in turn, change the products displayed.
Create an empty object in your updateFilters method. Loop through the topics and
populate the filters object with the items checked. Set the query parameters in the
router to the filters object:
updateFilters() {
let filters = {};
Object.keys(this.topics).forEach(key => {
let topic = this.topics[key];
if(topic.checked.length) {
filters[key] = topic.checked;
}
});
[ 340 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
this.$router.push({query: filters});
}
Checking and unchecking the filters on the right should update the URL with the items
checked.
Preselecting filters on page load
When loading a category with filters already in the URL, we need to ensure the checkboxes
are checked on the right-hand side. This can be done by looping through the existing query
parameters and adding any matching keys and arrays to the topics parameter. As the
query can either be an array or a string, we need to ensure the checked property is an array
no matter what. We also need to ensure the query key is, indeed, a filter and not a page
parameter:
filters() {
if(this.categoriesExist) {
let category = this.categoryProducts(this.slug);
for(let product of category.productDetails) {
...
}
Object.keys(this.$route.query).forEach(key => {
if(Object.keys(this.topics).includes(key)) {
let query = this.$route.query[key];
this.topics[key].checked = Array.isArray(query) ? query : [query];
}
});
}
return this.topics;
}
On page load, the filters in the URL will be checked.
[ 341 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
Filtering the products
Our filters are now being created and appended to dynamically, and activating a filter
updates the query parameter in the URL. We can now proceed with showing and hiding
products based on the URL parameters. We are going to be doing this by filtering the
products before being passed into the ListProducts component. This ensures the
pagination works correctly.
As we are filtering, open up ListProducts.js and add a :key attribute to each list item,
with the value of the handle:
...
Open up the CategoryPage view and create a method within the methods object titled
filtering() and add a return true to begin with. The method should accept two
parameters, a product and query object:
methods: {
filtering(product, query) {
return true;
}
}
Next, within the category computed function, we need to filter the products if there is a
query parameter. However, we need to be careful that we don't trigger the filters if the page
number is present – as that is also a query.
Create a new variable called filters, which is a copy of the query object from the route.
Next, if the page parameter is present, delete it from our new object. From there, we can
check whether the query object has any other contents and if so, run the native JavaScript
filter() function on our product array – passing in the product and new query/filters
object to our method:
category() {
if(this.categoriesExist) {
let category = this.categoryProducts(this.slug),
filters = Object.assign({}, this.$route.query);
if(Object.keys(filters).length && filters.hasProperty('page')) {
delete filters.page;
[ 342 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
}
if(Object.keys(filters).length) {
category.productDetails = category.productDetails.filter(
p => this.filtering(p, filters)
);
}
if(!category) {
this.categoryNotFound = true;
}
return category;
}
}
Refresh your app to ensure the products still show.
To filter products, there is quite a complex process involved. We want to check whether an
attribute is in the query parameters; if it is, we set a placeholder value of false. If the
attribute on the product matches that of the query parameter, we set the placeholder to
true. We then repeat this for each of the query parameters. Once complete, we then only
show products that have all of the criteria.
The way we are going to construct this allows products to be OR within the categories, but
AND with different sections. For example, if the user were to pick many colors (red and
green) and one tag (accessories), it will show all products that are red or green accessories.
Our filtering is created with the tags, vendor, and then dynamic filters. As two of the
properties are fixed, we will have to check these first. The dynamic filters will be verified by
reconstructing the way they were built.
Create a hasProperty object, which will be our placeholder object for keeping track of the
query parameters the product has. We'll begin with the vendor – as this is the simplest
property.
We start by looping through the query attributes – in case there is more than one (for
example, red and green). Next, we need to confirm that the vendor exists in the query – if
it does, we then set a vendor attribute in the hasProperty object to false. We then check
whether the vendor handle is the same as the query attribute. If this matches, we change
our hasProperty.vendor property to true:
filtering(product, query) {
let display = false,
hasProperty = {};
Object.keys(query).forEach(key => {
[ 343 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
let filter = Array.isArray(query[key]) ? query[key] : [query[key]];
for(attribute of filter) {
if(key == 'vendor') {
hasProperty.vendor = false;
if(product.vendor.handle == attribute) {
hasProperty.vendor = true;
}
}
}
});
return display;
}
This will update the hasProperty object with whether the vendor matches the selected
filter. We can row replicate the functionality with the tags – remembering that tags on a
product are an object we need to filter.
The dynamic properties constructed by the filters will also need to be checked. This is done
by checking the variant object on each variationProduct, and updating the
hasProperty object if it matches:
filtering(product, query) {
let display = false,
hasProperty = {};
Object.keys(query).forEach(key => {
let filter = Array.isArray(query[key]) ? query[key] : [query[key]];
for(attribute of filter) {
if(key == 'vendor') {
hasProperty.vendor = false;
if(product.vendor.handle == attribute) {
hasProperty.vendor = true;
}
} else if(key == 'tags') {
hasProperty.tags = false;
product[key].map(key => {
if(key.handle == attribute) {
hasProperty.tags = true;
}
[ 344 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
});
} else {
hasProperty[key] = false;
let variant = product.variationProducts.map(v => {
if(v.variant[key] && v.variant[key].handle == attribute) {
hasProperty[key] = true;
}
});
}
}
});
return display;
}
Lastly, we need to check each of the properties of the hasProperty object. If all the values
are set to true, we can set the display of the product to true – meaning it will show. If one
of them is false, the product will not show as it does not match all of the criteria:
filtering(product, query) {
let display = false,
hasProperty = {};
Object.keys(query).forEach(key => {
let filter = Array.isArray(query[key]) ? query[key] : [query[key]];
for(attribute of filter) {
if(key == 'vendor') {
hasProperty.vendor = false;
if(product.vendor.handle == attribute) {
hasProperty.vendor = true;
}
} else if(key == 'tags') {
hasProperty.tags = false;
product[key].map(key => {
if(key.handle == attribute) {
hasProperty.tags = true;
}
});
} else {
hasProperty[key] = false;
[ 345 ]
Building an E-Commerce Store - Browsing Products
Chapter 10
let variant = product.variationProducts.map(v => {
if(v.variant[key] && v.variant[key].handle == attribute) {
hasProperty[key] = true;
}
});
}
}
if(Object.keys(hasProperty).every(key => hasProperty[key])) {
display = true;
}
});
return display;
}
We now have a successful filtering product list. View your app in the browser and update
the filters – noting how products show and hide with each click. Note how even when you
press refresh, only the filtered products display.
Summary
In this chapter, we created a category listing page, allowing the user the view all the
products in a category. This list is able to be paginated, along with the order changing. We
also created a filtering component, allowing the user to narrow down the results.
With our products now browseable, filterable, and viewable, we can proceed on to making
a Cart and Checkout page.
[ 346 ]
11
Building an E-Commerce Store
- Adding a Checkout
Over the last couple of chapters, we have been creating an e-commerce store. So far, we
have created a product page that allows us to view the images and product variations,
which may be size or style. We have also created a category page with filters and
pagination—including a homepage category page that features specific, selected products.
Our users can browse and filter products and view more information about a specific
product. We are now going to:
Build the functionality to allow the user to add and remove products to their
basket
Allow a user to Checkout
Add an Order Confirmation page
As a reminder—we won't be taking any billing details but we will make an Order
Confirmation screen.
Creating the basket array placeholder
To help us persist the products in the basket throughout the app, we are going to be storing
the user's selected products in the Vuex store. This will be in the form of an array of objects.
Each object will contain several key pieces of information that will allow us to display the
products in the basket without having to query the Vuex store each time. It also allows us
to store details about the current state of the product page—remembering the image
updates when a variant is selected.
Building an E-Commerce Store - Adding a Checkout
Chapter 11
The details we're going to store for each product added to the basket are as follows:
Product title
Product handle, so we can link back to the product
Selected variation title (as it appears in the select box)
Currently selected image, so we can show an appropriate image in the Checkout
Variation details, this contains price and weight along with other details
Variation SKU, this will help us identify whether the product has already been
added
Quantity, how many items the user has added to their basket
As we will be storing all this information within an object, contained in an array, we need
to create a placeholder array within the store. Add a new key to the state object within the
store titled basket and make it a blank array:
const store = new Vuex.Store({
state: {
products: {},
categories: {},
categoryHome: {
title: 'Welcome to the Shop',
handle: 'home',
products: [
...
]
},
basket: []
},
mutations: {
...
},
actions: {
...
},
getters: {
...
}
});
[ 348 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
Adding product information to the store
With our basket array ready to receive data, we can now create a mutation to add the
product object. Open the ProductPage.js file and update the addToBasket method to
call a $store commit function, instead of the alert we put in place.
All of the information we require for products to be added to the basket is stored on the
ProductPage component—so we can pass the component instance through to the
commit() function using the this keyword. This will become clear when we build the
mutation.
Add the function call to the ProductPage method:
methods: {
...
addToBasket() {
this.$store.commit('addToBasket', this);
}
}
Creating the store mutation to add products to
the basket
Navigate to the Vuex store and create a new mutation titled addToBasket. This will accept
the state as the first parameter and the component instance as the second. Passing the
instance through allows us to access the variables, methods, and computed values on the
component:
mutations: {
products(state, payload) {
...
},
categories(state, payload) {
...
},
addToBasket(state, item) {
}
}
[ 349 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
We can now proceed with adding the products to the basket array. The first step is to add
the product object with the mentioned properties. As it's an array, we can use the push()
function to add the object.
Next, add an object to the array, using the item and its properties to build the object. With
access to the ProductPage component, we can construct the variant title as it appears in
the select box, using the variantTitle method. Set the quantity to 1 by default:
addToBasket(state, item) {
state.basket.push({
sku: item.variation.sku,
title: item.product.title,
handle: item.slug,
image: item.image,
variationTitle: item.variantTitle(item.variation),
variation: item.variation,
quantity: 1
});
}
This now adds the product to the basket array. An issue appears, however, when you add
two of the same item to the basket. Rather than increasing the quantity, it simply adds a
second product.
This can be remedied by checking if the sku exists within the array already. If it does, we
can increment the quantity on that item, if not we can add a new item to the basket array.
The sku is unique for each variation of each product. Alternatively, we could use the
barcode property.
Using the native find JavaScript function, we can identify any products that have an SKU
matching that of the one being passed in:
addToBasket(state, item) {
let product = state.basket.find(p => {
if(p.sku == item.variation.sku) {
}
});
state.basket.push({
sku: item.variation.sku,
title: item.product.title,
handle: item.slug,
image: item.image,
[ 350 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
variationTitle: item.variantTitle(item.variation),
variation: item.variation,
quantity: 1
});
}
If it matches, we can increment the quantity by one on that object, using the ++ notation in
JavaScript. If not, we can add the new object to the basket array. When using the find
function, we can return the product if it exists. If not, we can add a new item:
addToBasket(state, item) {
let product = state.basket.find(p => {
if(p.sku == item.variation.sku) {
p.quantity++;
return p;
}
});
if(!product) {
state.basket.push({
sku: item.variation.sku,
title: item.product.title,
handle: item.slug,
image: item.image,
variationTitle: item.variantTitle(item.variation),
variation: item.variation,
quantity: 1
});
}
}
We now have a basket being populated as the item is added to the basket, and
incrementing when it exists already.
To improve the usability of the app, we should give the user some feedback when they
have added an item to the basket. This can be done by updating the Add to Basket button
briefly and showing a product count with a link to the basket in the header of the site.
[ 351 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
Updating the Add to basket button when
adding an item
As a usability improvement to our shop, we are going to update the Add to basket button
when a user clicks it. This will change to Added to your basket and apply a class for a set
period of time, for example, two seconds, before returning to its previous state. The CSS
class will allow you to style the button differently—for example, changing the background
to green or transforming it slightly.
This will be achieved by using a data property on the component—setting it to true and
false as the item gets added. The CSS class and text will use this property to determine
what to show and a setTimeout JavaScript function will change the state of the property.
Open the ProductPage component and add a new key to the data object titled
addedToBasket. Set this to false by default:
data() {
return {
slug: this.$route.params.slug,
productNotFound: false,
image: false,
variation: false,
addedToBasket: false
}
}
Update the button text to allow for this variation. As there is already a ternary if, within
this, we are going to nest them with another one. This could be abstracted into a method if
desired.
Replace the Add to basket condition in your button with an additional ternary operator,
dependent on whether the addedToBasket variable is true. We can also add a conditional
class based on this property:
{{
(variation.quantity) ?
((addedToBasket) ? 'Added to your basket' : 'Add to basket') :
'Out of stock'
[ 352 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
}}
Refresh the app and navigate to a product to ensure the correct text is being shown. Update
the addedToBasket variable to true to make sure everything is displaying as it should.
Set it back to false.
Next, within the addToBasket() method, set the property to true. This should update the
text when the item is added to the basket:
addToBasket() {
this.$store.commit('addToBasket', this);
this.addedToBasket = true;
}
When you click the button, the text will now update, however it will never reset. Add a
setTimeout JavaScript function afterward, which sets it back to false after a certain
period of time:
addToBasket() {
this.$store.commit('addToBasket', this);
this.addedToBasket = true;
setTimeout(() => this.addedToBasket = false, 2000);
}
The timing for setTimeout is in milliseconds, so 2000 is equal to two seconds. Feel free to
tweak and play with this figure as much as you see fit.
One last addition would be to reset this value back to false if the variation is updated or
the product is changed. Add the statement to both watch functions:
watch: {
variation(v) {
if(v.hasOwnProperty('image')) {
this.updateImage(v.image);
}
this.addedToBasket = false;
},
'$route'(to) {
this.slug = to.params.slug;
this.addedToBasket = false;
}
}
[ 353 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
Showing the product count in the header of
the app
It's common practice for a shop to show a link to the cart in the site's header—along with
the number of items in the cart next to it. To achieve this, we'll use a Vuex getter to calculate
and return the number of items in the basket.
Open the index.html file and add a element to the app HTML and insert a
placeholder, span—we'll convert this to a link once we've set up the routes. Within the
span, output a cartQuantity variable:
Navigate to your Vue instance and create a computed object containing a cartQuantity
function:
new Vue({
el: '#app',
store,
router,
computed: {
cartQuantity() {
}
},
created() {
CSV.fetch({url: './data/csv-files/bicycles.csv'}).then(data => {
this.$store.dispatch('initializeShop', this.$formatProducts(data));
});
}
});
[ 354 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
If our header were to feature more items than our cart link, it would be advisable to
abstract it out into a separate component to keep the methods, layout, and functions
contained. However, as it is only going to feature this one link in our example app, adding
the function to the Vue instance will suffice.
Create a new getter in the store titled cartQuantity. As a placeholder, return 1. The
state will be required to calculate the quantity, so ensure that is passed into the function
for now:
getters: {
...
cartQuantity: (state) => {
return 1;
}
}
Head back to your Vue instance and return the result of the getter. Ideally, we want to show
the count of the basket in brackets, but we only want to show the brackets if there are
items. Within the computed function, check the result of this getter and output the result
with brackets if the result exists:
cartQuantity() {
const quantity = this.$store.getters.cartQuantity;
return quantity ? `(${quantity})` : '';
}
Changing the result within the Vuex getter should reveal either the number in brackets or
nothing at all.
Calculating the basket quantity
With the display logic in place, we can now proceed with calculating how many items are
in the basket. We could count the number of items in the basket array, however, this will
only tell us how many different products are there now and not if the same product was
added many times.
[ 355 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
Instead, we need to loop through each product in the basket and add the quantities
together. Create a variable called quantity and set it to 0. Loop through the basket items
and add the item.quantity variable to the quantity variable. Lastly, return our variable
with the right sum:
cartQuantity: (state) => {
let quantity = 0;
for(let item of state.basket) {
quantity += item.quantity;
}
return quantity;
}
Navigate to the app and add some items to your basket to verify the basket count is being
calculated correctly.
Finalizing the Shop Vue-router URLs
We're now at a stage where we can finalize the URLs for our shop - including creating the
redirects and Checkout links. Referring back to Chapter 8, Introducing Vue-Router and
Loading URL-Based Components, we can see which ones we are missing. These are:
/category -redirect to /
/product - redirect to /
/basket - load OrderBasket component
/checkout- load OrderCheckout component
/complete- load OrderConfirmation component
Create the redirects in the appropriate places within the routes array. At the bottom of the
routes array, create three new routes for the Order components:
routes: [
{
path: '/',
name: 'Home',
...
},
{
path: '/category',
redirect: {name: 'Home'}
},
{
path: '/category/:slug',
[ 356 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
name: 'Category',
...
},
{
path: '/product',
redirect: {name: 'Home'}
},
{
path: '/product/:slug',
name: 'Product',
component: ProductPage
},
{
path: '/basket',
name: 'Basket',
component: OrderBasket
},
{
path: '/checkout',
name: 'Checkout',
component: OrderCheckout
},
{
path: '/complete',
name: 'Confirmation',
component: OrderConfirmation
},
{
path: '/404',
alias: '*',
component: PageNotFound
}
]
We can now update the placeholder in the header of our app with a router-link:
[ 357 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
Building the Order process and
ListProducts component
For the three steps of the Checkout, we are going to be utilizing the same component in all
three: the ListProducts component. In the OrderCheckout, and OrderConfirmation
components, it will be in a fixed, uneditable state, whereas when it is in the OrderBasket
component, the user needs to be able to update quantities and remove items if desired.
As we're going to be working at the Checkout, we need products to exist in the basket
array. To save us having to find products and add them to the basket every time we refresh
the app, we can ensure there are some products in the basket array by hardcoding an
array in the store.
To achieve this, navigate to a few products and add them to your basket. Ensure there is a
good selection of products and quantities for testing. Next, open your JavaScript console in
the browser and enter the following command:
console.log(JSON.stringify(store.state.basket));
This will output a string of your products array. Copy this and paste it into your
store—replacing the basket array:
state: {
products: {},
categories: {},
categoryHome: {
title: 'Welcome to the Shop',
handle: 'home',
products: [
...
]
},
basket: [{"sku":...}]
},
On page load, your Cart count in the header should update to be the correct number of
items you added.
[ 358 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
We can now proceed with building our Checkout process. The product display in the
basket is more complicated than the Checkout and Order Confirmation screens so,
unusually, we'll work backward. Starting with the Order Confirmation page and moving
to the Checkout page, adding more complexity before we head to the basket, adding the
ability to exit the products.
Order Confirmation screen
The Order Confirmation screen is one that is shown once the order is complete. This
confirms the items purchased and may include the expected delivery date.
Create a template within the OrderConfirmation.js file with a and some relevant
content relating to the order being complete:
const OrderConfirmation = {
name: 'OrderConfirmation',
template: `
Order Complete!
Thanks for shopping with us - you can expect your products within 2
- 3 working days
`
};
Open up the application in your browser, add a product to your basket and complete an
order to confirm it's working. The next step is to include the ListProducts component.
First, ensure the ListProducts component is correctly initialized and features an initial
template:
const ListPurchases = {
name: 'ListPurchases',
template: ` `
};
Add the components object to the OrderConfirmation component and include the
ListProducts component. Next, include it in the template:
const OrderConfirmation = {
name: 'OrderConfirmation',
template: `
Order Complete!
Thanks for shopping with us - you can expect your products within 2
- 3 working days
[ 359 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
`,
components: {
ListPurchases
}
};
Open the ListPurchases component once more to start displaying the products. The
default state of this component will be to list the products in the basket, along with the
variation selected. The price for each product will be displayed, along with the price if the
quantity is more than one. Lastly, a grand total will be shown.
The first step is to get the basket list into our component. Create a computed object with a
products function. This should return the basket products:
const ListPurchases = {
name: 'ListPurchases',
template: ``,
computed: {
products() {
return this.$store.state.basket;
}
}
};
With the products in the basket now available to us, we can loop through them in the table
displaying the information required. This includes a thumbnail image, the product and
variation title, price, quantity, and the total price of the item. Add a header row to the table
too, so the user knows what the column is:
template: `
Title
Unit price
Quantity
Price
{{ product.title }}
{{ product.variationTitle }}
{{ product.variation.price }}
{{ product.quantity }}
{{ product.variation.price * product.quantity }}
`,
Note that the price for each row is simply the unit price multiplied by the quantity. We now
have a standard product list of the items the user has purchased.
Using Vue filters to format the price
The price is currently an integer, as that it is in the data. On the product page, we just
prepended a $ sign to represent a price, however, this is now the perfect opportunity to
utilize Vue filters. Filters allow you to manipulate the data in the template without using a
method. Filters can be chained and are used to carry out, generally, a single
modification—for example converting a string to lowercase or formatting a number to be a
currency.
Filters are used with the pipe (|) operator. If, for example, we had a filter to lowercase text,
it would be used like the following:
{{ product.title | lowercase }}
Filters are declared within a filters object on the component and accept a single
parameter of the output preceding it.
[ 361 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
Create a filters object within the ListPurchases component and create a function
inside titled currency(). This function accepts a single parameter of val and should
return the variable inside:
filters: {
currency(val) {
return val;
}
},
We can now use this function to manipulate the price integers. Add the filter to both the
unit and total price within the template:
{{ product.variation.price | currency }}
{{ product.quantity }}
{{ product.variation.price * product.quantity | currency }}
You won't notice anything in the browser, as we have yet to manipulate the value. Update
the function to ensure the number is fixed to two decimal places and has a $ preceding it:
filters: {
currency(val) {
return ' + val.toFixed(2);
}
},
Our prices are now nicely formatted and displaying correctly.
Calculating a total price
The next addition to our purchase list is a total value of the basket. This will need to be
calculated in a similar way to the basket count we did earlier.
Create a new computed function title: totalPrice. This function should loop through the
products and add the price up, taking into consideration any multiple quantities:
totalPrice() {
let total = 0;
for(let p of this.products) {
total += (p.variation.price * p.quantity);
}
return total;
}
[ 362 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
We can now update our template to include the total price—ensuring we pass it through
the currency filter:
template: `
Title
Unit price
Quantity
Price
{{ product.title }}
{{ product.variationTitle }}
{{ product.variation.price | currency }}
{{ product.quantity }}
{{ product.variation.price * product.quantity | currency }}
Total:
{{ totalPrice | currency }}
`,
[ 363 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
Creating an Order Checkout page
Our OrderCheckout page will have a similar makeup to the OrderConfirmation page however, in a real shop, this would be the page before payment. This page would allow the
user to fill in their billing and delivery details before navigating to the payment page. Copy
the OrderConfirmation page and update the title and info text:
const OrderCheckout = {
name: 'OrderCheckout',
template: ';
Order Confirmation
Please check the items below and fill in your details to complete
your order
',
components: {
ListPurchases
}
};
Below the component, create a form with several fields so we can
collect the billing and delivery name and addresses. For this example, just collect the name,
first line of the address, and ZIP code:
template: '
Order Confirmation
Please check the items below and fill in your details to complete your
order
',
We now need to create a data object and bind each field to a key. To help group each set,
create an object for both delivery and billing and create the fields inside with the
correct names:
data() {
return {
billing: {
name: '',
address: '',
zipcode: ''
},
delivery: {
name: '',
address: '',
zipcode: ''
}
}
}
Add a v-model to each input, linking it to the appropriate data key:
[ 365 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
The next step is to create a submit method and collate the data to be able to pass it on to
the next screen. Create a new method titled submitForm(). As we are not handling
payment in this example, we can route to the confirmation page in the method:
methods: {
submitForm() {
// this.billing = billing details
// this.delivery = delivery details
this.$router.push({name: 'Confirmation'});
}
}
We can now bind a submit event to the form and add a submit button. Like the vbind:click attribute (or @click), Vue allows you to bind a submit event to a method
using a @submit="" attribute.
Add the declaration to the
On submitting your form, the app should redirect you to our Confirmation page.
Copying details between addresses
One feature that several shops have is the ability to mark the delivery address to be the
same as the billing address. There are several ways we could approach this, and how you
choose to do it is up to you. The immediate options are:
Have a "copy details" button—this copies the details from billing to delivery but
does not keep them in sync
[ 366 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
Have a checkbox that keeps the two in sync—checking the box disables the
delivery box fields but populates them with the billing details
For this example, we are going to code the second option.
Create a checkbox between the two fieldsets that is bound to a property in the data object
via v-model called sameAddress:
Create a new key in the data object and set it to false by default:
data() {
return {
sameAddress: false,
billing: {
name: '',
address: '',
zipcode: ''
},
delivery: {
name: '',
address: '',
zipcode: ''
}
}
},
[ 367 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
The next step is to disable the delivery fields if the checkbox is checked. This can be done by
activating the disabled HTML attribute based on the checkbox result. In a similar way to
how we disabled the "Add to cart" button on the product page, bind the disabled attribute
on the delivery fields to the sameAddress variable:
Delivery Details
Name:
Address:
Post code/Zip code:
Checking the box will now deactivate the fields - making the user unable to enter any data.
The next step is to replicate the data across the two sections. As our data objects are the
same structure, we can create a watch function to set the delivery object to the same as
the billing object when the checkbox is checked.
Create a new watch object and function for the sameAddress variable. If it is true, set the
delivery object to the same as the billing one:
watch: {
sameAddress() {
if(this.sameAddress) {
this.delivery = this.billing;
}
}
}
With the watch function added, we can enter data into the billing address, check the box,
and the delivery address gets populated. The best thing about this is that they now stay in
sync, so if you update the billing address, the delivery address updates on the fly. The
problem arises when you uncheck the box and edit the billing address—the delivery
address still updates. This is because we have bound the objects together.
[ 368 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
Add an else statement to make a copy of the billing address when the box is unchecked:
watch: {
sameAddress() {
if(this.sameAddress) {
this.delivery = this.billing;
} else {
this.delivery = Object.assign({}, this.billing);
}
}
}
We now have a functioning Order Confirmation page, which collects billing and delivery
details.
Creating an editable basket
We now need to create our basket. This needs to show the products in a similar fashion to
the Checkout and Confirmation, but it needs to give the user the ability to edit the basket
contents—either to delete an item or update the quantity.
As a starting point, open OrderBasket.js and include the list-purchases component,
as we did on the confirmation page:
const OrderBasket = {
name: 'OrderBasket',
template: `
Basket
`,
components: {
ListPurchases
}
};
[ 369 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
The next thing we need to do is edit the list-purchases component. To ensure we can
differentiate between the views, we are going to add an editable prop. This will be set to
false by default and true in the basket. Add the prop to the component in the basket:
template: `
Basket
`,
We now need to tell the ListPurchases component to accept this parameter so we can do
something with it within the component:
props: {
editable: {
type: Boolean,
default: false
}
},
Creating editable fields
We now have a prop determining if our basket should be editable or not. This allows us to
show the delete links and make the quantity an editable box.
Create a new table cell next to the quantity one in the ListPurchases component and
make it visible only when the purchases are visible. Make the static quantity hidden in this
state too. In the new cell, add an input box with the value set to the quantity. We are also
going to bind a blur event to the box. The blur event is a native JavaScript event that
triggers when the input is unfocused. On blur, trigger an updateQuantity method. This
method should accept two arguments; the event, which will contain the new quantity, and
the SKU for that particular product:
{{ product.title }}
[ 370 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
{{ product.variationTitle }}
{{ product.variation.price | currency }}
{{ product.quantity }}
{{ product.variation.price * product.quantity | currency }}
Create the new method on the component. This method should loop through the products,
locating the one with a matching SKU and updating the quantity to an integer. We also
need to update the store with the result - so that the quantity can be updated at the top of
the page. We'll create a general mutation that accepts the full basket array back with new
values to allow the same one to be used for the product deletion.
Create the mutation that updates the quantity and commits a mutation
titled updatePurchases:
methods: {
updateQuantity(e, sku) {
let products = this.products.map(p => {
if(p.sku == sku) {
p.quantity = parseInt(e.target.value);
}
return p;
});
this.$store.commit('updatePurchases', products);
}
}
In the store, create the mutation that sets the state.basket equal to the payload:
updatePurchases(state, payload) {
state.basket = payload;
}
Updating the quantity should now update the total price of the item and the basket count at
the top of the page.
[ 371 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
Removing items from your cart
The next step is to give the user the ability to remove items from their cart. Create a button
in the ListPurchases component with a click binding. This button can go anywhere you
want - our example shows it as an extra cell at the end of the row. Bind the click action to a
method titled removeItem. This just needs to accept a single parameter of the SKU. Add
the following to the ListPurchases component:
{{ product.title }}
{{ product.variationTitle }}
{{ product.variation.price | currency }}
{{ product.quantity }}
{{ product.variation.price * product.quantity | currency }}
Remove item
[ 372 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
Create the removeItem method. This method should filter the basket array, only
returning the objects that don't match the SKU passed in. Once the result is filtered, pass the
result to the same mutation we used in the updateQuantity() method:
removeItem(sku) {
let products = this.products.filter(p => {
if(p.sku != sku) {
return p;
}
});
this.$store.commit('updatePurchases', products);
}
One last enhancement we can make is to trigger the removeItem method if the quantity is
set to 0. Within the updateQuantity method, check the value before looping through the
products. If it is 0, or doesn't exist, run the removeItem method - passing the SKU through:
updateQuantity(e, sku) {
if(!parseInt(e.target.value)) {
this.removeItem(sku);
} else {
let products = this.products.map(p => {
if(p.sku == sku) {
p.quantity = parseInt(e.target.value);
}
return p;
});
this.$store.commit('updatePurchases', products);
}
},
Completing the shop SPA
The last step is to add a link from the OrderBasket component to the OrderCheckout
page. This can be done by linking to the Checkout route. With that, your checkout is
complete, along with your shop! Add the following link to the basket:
template: `
Basket
Proceed to Checkout
`,
[ 373 ]
Building an E-Commerce Store - Adding a Checkout
Chapter 11
Summary
Well done! You have created a full shop single-page application using Vue.js. You have
learned how to list products and their variations, along with adding specific variations to
the basket. You've learned how to create shop filters and category links, along with creating
an editable shopping basket.
As with everything, there are always improvements to be made. Why don't you give some
of these ideas a go?
Persisting the basket using localStorage—so products added to the basket are
retained between visits and the user pressing refresh
Calculating shipping based on the weight attribute of the products in the
basket—use a switch statement to create bands
Allowing products without variations to be added to the basket from the
category listing page
Indicating which products have items out of stock when filtered on that variation
on the category page
Any ideas of your own!
[ 374 ]
12
Using Vue Dev Tools and
Testing Your SPA
Over the last 11 chapters, we've been developing several Single-Page Applications (SPAs)
using Vue.js. Although development is a big chunk of creating an SPA, testing also forms
a significant part of creating any JavaScript web app.
The Vue developer tools, available in Chrome and Firefox, provide great insights into the
components being used within a certain view or the current state of the Vuex store – along
with any events being emitted from the JavaScript. These tools allow you to check and
validate the data within your app while developing to ensure everything is as it should be.
The other side of SPA testing is with automated tests. Conditions, rules, and routes you
write to automate tasks within your app, allow you to then specify what the output should
be and the test runs the conditions to verify whether the results match.
In this chapter, we will:
Cover the usage of the Vue developer tools with the applications we've
developed
Have an overview of testing tools and applications
Using Vue Dev Tools and Testing Your SPA
Chapter 12
Using the Vue.js developer tools
The Vue developer tools are available for Chrome and Firefox and can be downloaded from
GitHub (https://github.com/vuejs/vue-devtools). Once installed, they become an
extension of the browser developer tools. For example, in Chrome, they appear after the
Audits tab.
The Vue developer tools will only work when you are using Vue in development mode. By
default, the unminified version of Vue has the development mode enabled. However, if
you are using the production version of the code, the development tools can be enabled by
setting the devtools variable to true in your code:
Vue.config.devtools = true
Throughout the book, we've been using the development version of Vue, so the dev tools
should work with all three of the SPAs we have developed. Open the Dropbox example
and open the Vue developer tools.
Inspecting Vue components data and computed
values
The Vue developer tools give a great overview of the components in use on the page. You
can also drill down into the components and preview the data in use on that particular
instance. This is perfect for inspecting the properties of each component on the page at any
given time.
For example, if we inspect the Dropbox app and navigate to the Components tab, we can
see the Vue instance and we can see the component. Clicking
this will reveal all of the data properties of the component – along with any computed
properties. This lets us validate whether the structure is constructed correctly, along with
the computed path property:
[ 376 ]
Using Vue Dev Tools and Testing Your SPA
Chapter 12
Drilling down into each component, we can access individual data objects and computed
properties.
Using the Vue developer tools for inspecting your application is a much more efficient way
of validating data while creating your app, as it saves having to place several
console.log() statements.
Viewing Vuex mutations and time-travel
Navigating to the next tab, Vuex, allows us to watch store mutations taking place in real
time. Every time a mutation is fired, a new line is created in the left-hand panel. This
element allows us to view what data is being sent, and what the Vuex store looked like
before and after the data had been committed.
[ 377 ]
Using Vue Dev Tools and Testing Your SPA
Chapter 12
It also gives you several options to revert, commit, and time-travel to any point. Loading
the Dropbox app, several structure mutations immediately populate within the left-hand
panel, listing the mutation name and the time they occurred. This is the code pre-caching
the folders in action. Clicking on each one will reveal the Vuex store state – along with a
mutation containing the payload sent. The state display is after the payload has been sent
and the mutation committed. To preview what the state looked like before that mutation,
select the preceding option:
On each entry, next to the mutation name, you will notice three symbols that allow you to
carry out several actions and directly mutate the store in your browser:
Commit this mutation: This allows you to commit all the data up to that point.
This will remove all of the mutations from the dev tools and update the Base State
to this point. This is handy if there are several mutations occurring that you wish
to keep track of.
[ 378 ]
Using Vue Dev Tools and Testing Your SPA
Chapter 12
Revert this mutation: This will undo the mutation and all mutations after this
point. This allows you to carry out the same actions again and again without
pressing refresh or losing your current place. For example, when adding a
product to the basket in our shop app, a mutation occurs. Using this would allow
you to remove the product from the basket and undo any following mutations
without navigating away from the product page.
Time-travel to this state: This allows you to preview the app and state at that
particular mutation, without reverting any mutations that occur after the selected
point.
The mutations tab also allows you to commit or revert all mutations at the top of the lefthand panel. Within the right-hand panel, you can also import and export a JSON encoded
version of the store's state. This is particularly handy when you want to re-test several
circumstances and instances without having to reproduce several steps.
Previewing event data
The Events tab of the Vue developer tools works in a similar way to the Vuex tab, allowing
you to inspect any events emitted throughout your app. Our Dropbox app doesn't use
events, so open up the people-filtering app we created in Chapter 2, Displaying, Looping,
Searching, and Filtering Data, and Chapter 3, Optimizing our App and Using Components to
Display Data, of this book.
Changing the filters in this app emits an event each time the filter type is updated, along
with the filter query:
[ 379 ]
Using Vue Dev Tools and Testing Your SPA
Chapter 12
The left-hand panel again lists the name of the event and the time it occurred. The right
panel contains information about the event, including its component origin and payload.
This data allows you to ensure the event data is as you expected it to be and, if not, helps
you locate where the event is being triggered.
The Vue dev tools are invaluable, especially as your JavaScript application gets bigger and
more complex. Open the shop SPA we developed and inspect the various components and
Vuex data to get an idea of how this tool can help you create applications that only commit
mutations they need to and emit the events they have to.
Testing your SPA
The majority of Vue testing suites revolve around having command-line knowledge and
creating a Vue application using the CLI (command-line interface). Along with creating
applications in frontend-compatible JavaScript, Vue also has a CLI that allows you to create
applications using component-based files. These are files with a .vue extension and contain
the template HTML along with the JavaScript required for the component. They also allow
you to create scoped CSS – styles that only apply to that component. If you chose to create
your app using the CLI, all of the theory and a lot of the practical knowledge you have
learned in this book can easily be ported across.
Command-line unit testing
Along with component files, the Vue CLI allows you to integrate with command-line unit
tests easier, such as Jest, Mocha, Chai, and TestCafe (https://testcafe.devexpress.com/).
For example, TestCafe allows you to specify several different tests, including checking
whether content exists, to clicking buttons to test functionality. An example of a TestCafe
test checking to see if our filtering component in our first app contains the work Field
would be:
test('The filtering contains the word "filter"', async testController => {
const filterSelector = await new Selector('body > #app > form >
label:nth-child(1)');
await testController.expect(paragraphSelector.innerText).eql('Filter');
});
[ 380 ]
Using Vue Dev Tools and Testing Your SPA
Chapter 12
This test would then equate to true or false. Unit tests are generally written in
conjunction with components themselves, allowing components to be reused and tested in
isolation. This allows you to check that external factors have no bearing on the output of
your tests.
Most command-line JavaScript testing libraries will integrate with Vue.js; there is a great
list available in the awesome Vue GitHub repository (https://github.com/vuejs/
awesome-vue#test).
Browser automation
The alternative to using command-line unit testing is to automate your browser with a
testing suite. This kind of testing is still triggered via the command line, but rather than
integrating directly with your Vue application, it opens the page in the browser and
interacts with it like a user would. A popular tool for doing this is Nightwatch.js (http:/
/nightwatchjs.org/).
You may use this suite for opening your shop and interacting with the filtering component
or product list ordering and comparing the result. The tests are written in very colloquial
English and are not restricted to being on the same domain name or file network as the site
to be tested. The library is also language agnostic – working for any website regardless of
what it is built with.
The example Nightwatch.js gives on their website is for opening Google and ensuring
the first result of a Google search for rembrandt van rijn is the Wikipedia entry:
module.exports = {
'Demo test Google' : function (client) {
client
.url('http://www.google.com')
.waitForElementVisible('body', 1000)
.assert.title('Google')
.assert.visible('input[type=text]')
.setValue('input[type=text]', 'rembrandt van rijn')
.waitForElementVisible('button[name=btnG]', 1000)
.click('button[name=btnG]')
.pause(1000)
.assert.containsText('ol#rso li:first-child',
'Rembrandt - Wikipedia')
.end();
}
};
[ 381 ]
Using Vue Dev Tools and Testing Your SPA
Chapter 12
An alternative to Nightwatch is Selenium (http://www.seleniumhq.org/). Selenium has
the advantage of having a Firefox extension that allows you to visually create tests and
commands.
Testing, especially for big applications, is paramount – especially when deploying your
application to a development environment. Whether you choose unit testing or browser
automation, there is a host of articles and books available on the subject.
Summary
Till now, we created a mock shop. Using real data from Shopify CSV files, we created an
application that allowed products to be viewed individually. We also created a category
listing page that could be filtered and ordered, allowing the user to find specifically the
products they wanted. To complete the experience, we built an editable Basket, Checkout,
and Order Confirmation screen. In this chapter, we covered the use of the Vue dev tools,
followed by how to build tests.
[ 382 ]
13
Transitions and Animations
In this chapter, the following recipes will be covered:
Integrating with third-party CSS animation libraries such as animate.css
Adding your own transition classes
Animating with JavaScript instead of CSS
Transitioning on the initial render
Transitioning between elements
Letting an element leave before the enter phase in a transition
Adding entering and leaving transitions for elements of a list
Transitioning elements that move in a list
Animating the state of your components
Packaging reusable transitions into components
Dynamic transitions
Introduction
This chapter contains recipes related to transitions and animations. Vue has its own tags for
dealing with transitions intended for when an element enters or leaves the scene:
and . You will learn all about them and how to use
them to give your customers a better user experience.
Transitions and Animations
Chapter 13
Vue transitions are pretty powerful in that they are completely customizable and can easily
combine JavaScript and CSS styling while having very intuitive defaults that will let you
write less code in case you don't want all the frills.
You can animate a great deal of what happens in your components even without transition
tags since all you have to do is bind your state variables to some visible property.
Finally, once you have mastered everything that there is to know about Vue transitions and
animations, you can easily package these in layered components and reuse them
throughout your application. This is what makes them not only powerful, but also easy to
use and maintain.
Integrating with third-party CSS animation
libraries such as animate.css
Graphical interfaces not only need to be usable and easy to understand; they should also
provide affordability and be pleasant to use. Having transitions can help a great deal by
giving cues of how a website works in a fun way. In this recipe, we will examine how to use
a CSS library with our application.
Getting ready
Before starting, you can take a look at https://daneden.github.io/animate.css/, as
shown, just to get an idea of the available animations, but you don't really need any special
knowledge to proceed:
[ 384 ]
Transitions and Animations
Chapter 13
How to do it...
Imagine that you are creating an app to book taxis. The interface we will create will be
simple and fun.
First of all, add the animate.css library to the list of dependencies (refer to the Choosing a
development environment recipe to learn how to do it).
To proceed, we need our usual wrapper:
Inside, we will put a button to call for a taxi:
Call a cab
[ 385 ]
Transitions and Animations
Chapter 13
You can already tell that we will use the taxiCalled variable to keep track of whether the
button has been pressed or not
Let's add an emoji that will confirm to the user when the taxi is called:
At this point, we can add some JavaScript:
new Vue({
el: '#app',
data: {
taxiCalled: false
}
})
Run the application and you will see the taxi appear instantly when you press the button.
We are a cool taxi company, so let's make the taxi drive to us with a transition:
Now run your application; if you call the taxi, it will get to you by sliding from the right:
The taxi will slide from right to left, as shown:
[ 386 ]
Transitions and Animations
Chapter 13
How does it work...
Every transition applies four classes. Two are applied when the element enters the
scene and the other two are applied when it leaves:
Name
Applied when
Removed when
v-enter
Before the element is inserted After one frame
v-enter-active Before the element is inserted When transition ends
v-enter-to
After one frame
When transition ends
v-leave
Transition starts
After one frame
v-leave-active Transition starts
When transition ends
v-leave-to
After one frame
When transition ends
Here, the initial v stands for the name of your transition. If you don't specify a name, v will
be used.
While the beginning of the transition is a well-defined instant, the end of
the transition is a bit of work for the browser to figure out. For example, if
a CSS animation loops, the duration of the animation will only be one
iteration. Also, this may change in future releases, so keep this in mind.
In our case, we wanted to provide a third-party v-enter-active instead of writing our
own. The problem is that our library already has a different name for the class of the
animation we want to use (slideInRight). Since we can't change the name of the class, we
tell Vue to use slideInRight instead of looking for a v-enter-active class.
To do this, we used the following code:
This means that our v-enter-active is called animated slideInRight now. Vue will
append those two classes before the element is inserted and drop them when the transition
ends. Just note that animated is a kind of helper class that comes with animate.css.
[ 387 ]
Transitions and Animations
Chapter 13
Adding your own transition classes
If your application is rich in animations and you would like to reuse your CSS classes in
other projects by mixing and matching them, this is the recipe for you. You will also
understand an important technique for performant animations, called FLIP (First Last
Invert Play). While the latter technique is normally triggered automatically by Vue, we will
implement it manually to get a better understanding of how it works.
Getting ready
To complete this recipe, you should understand how CSS animations and transitions work.
This is out of the scope of this book, but you can find a good primer
at http://css3.bradshawenterprises.com/. This website is also great because it will
explain when you can use animations and transitions.
How to do it...
We will build an interface for a taxi company (similar to the preceding recipe) that will
enable users to call a taxi at the click of a button and will provide a nice animated feedback
when the taxi is called.
To code the button, write the following HTML:
Then, you initialize the taxiCalled variable to false, as shown in the following
JavaScript:
new Vue({
el: '#app',
data: {
taxiCalled: false
}
})
[ 388 ]
Transitions and Animations
Chapter 13
At this point, we will create our own custom transition in CSS:
.slideInRight {
transform: translateX(200px);
}
.go {
transition: all 2s ease-out;
}
Wrap your car emoji in a Vue transition:
When you run your code and hit the Call a cab button, you will see a taxi stopping by.
How it works...
When we click on the button, the taxiCalled variable turns true and Vue inserts the taxi
into your page. Before actually doing this, it reads the classes you specified in enterclass (in this case, only slideInRight) and applies it to the wrapped element (the
element with the taxi emoji). It also applies the classes specified in enter-class-active
(in this case, only go).
The classes in enter-class are removed after the first frame, and the classes in enterclass-active are also removed when the animation ends.
The animation created here follows the FLIP technique that is composed of four points:
First (F): You take the property as it is in the first frame of your animation; in our
case, we want the taxi to start somewhere from the right of the screen.
Last (L): You take the property as it is in the last frame of your animation, which
is the taxi at the left of the screen in our case.
Invert (I): You invert the property change you registered between the first and
last frame. Since our taxi moved to the left, at the final frame it will be at say -200
pixel offset. We invert that and set the slideInRight class to have transform
as translateX(200px) so that the taxi will be at +200 pixel offset when it
appears.
[ 389 ]
Transitions and Animations
Chapter 13
Play (P): We create a transition for every property we have touched. In the taxi
example, we use the transform property and so, we use writetransition:
all 2s ease-out to tween the taxi smoothly.
This technique is used automatically by Vue under the cover to make transitions work
inside the tag. More on that in the Adding entering and leaving
transition for elements of a list recipe.
Animating with JavaScript instead of CSS
It's a common misconception that animating with JavaScript is slower and that animations
should be done in CSS. The reality is that if used correctly, animation in JavaScript can have
similar or superior performance. In this recipe, we will create an animation with the help of
the simple but powerful Velocity.js (http://velocityjs.org/) library:
[ 390 ]
Transitions and Animations
Chapter 13
Getting ready
This recipe, while it presupposes no knowledge of the Velocity library, assumes that you
are quite familiar with animations either in CSS or with JavaScript libraries, such as jQuery.
If you've never seen a CSS animation and you want a speedy introduction, just complete
the two preceding recipes and you should be able to follow along.
How to do it...
We're still looking for the perfect transition for a taxi company (the same as in the
preceding recipe) that will entertain our clients while waiting for a taxi. We have a button
to call a cab and a little taxi emoji that will appear when we make a reservation.
Before anything else, add the Velocity library as a dependency to your project--https://
cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js.
Here is the HTML to create the skeleton of our interface:
Our Vue model is very simple and consists only of the taxiCalled variable:
new Vue({
el: '#app',
data: {
taxiCalled: false
}
})
Create the animation by wrapping the little taxi in a Vue transition:
[ 391 ]
Transitions and Animations
Chapter 13
The enter method will be called as soon as the taxi emoji is inserted at the press of a button.
The enter method, which you have to add to your Vue instance, looks like this:
methods: {
enter (el) {
Velocity(el,
{ opacity: [1, 0], translateX: ["0px", "200px"] },
{ duration: 2000, easing: "ease-out" })
}
}
Run your code and press the button to book your taxi!
How it works...
As you may have noted, there is no CSS in your code. The animation is purely driven by
JavaScript. Let's dissect our Vue transition a little:
Although this is still a transition that could use CSS, we want to tell Vue to shut down the
CSS and save precious CPU cycles by setting :css="false". This will make Vue skip all
the code related to CSS animation and will prevent CSS from interfering with our pure
JavaScript animation.
The juicy part is in the @enter="enter" bit. We are binding the hook that triggers when
the element is inserted in to the enter method. The method itself is as follows:
enter (el) {
Velocity(el,
{ opacity: [1, 0], translateX: ["0px", "200px"] },
{ duration: 2000, easing: "ease-out" }
)
}
Here, we are calling the Velocity library. The el parameter is passed for free by Vue, and it
refers to the element that was inserted (in our case, the element containing the emoji of
the car).
[ 392 ]
Transitions and Animations
Chapter 13
The syntax of the Velocity function is as illustrated:
Velocity( elementToAnimate, propertiesToAnimate, [options] )
Other syntaxes are possible, but we will stick to this one.
In our call to this function, we passed our paragraph element as the first argument; we then
said that the opacity should change from 0 to 1 and, at the same time, the element should
move from a starting position of 200 pixels on the x axis toward its origin. As options, we
specified that the animation should last for two seconds and that we want to ease the
animation near the end.
I think everything is pretty clear maybe except how we are passing the opacity and
translateX parameters.
This is what Velocity calls forcefeeding--we are telling Velocity that the opacity should
start from 0 and go to 1. Likewise, we are telling Velocity that the translateX property
should start at 200 pixels, ending at 0 pixels.
In general, we can avoid passing arrays to specify the initial value for the properties;
Velocity will calculate how to transition.
For example, we could have had the following CSS class:
p {
opacity: 0;
}
If we rewrite the Velocity call as follows:
Velocity(el,
{ opacity: 1 }
)
The car will slowly appear. Velocity queried the DOM for the initial value of the element
and then transitioned it to 1. The problem with this approach is that since a query to the
DOM is involved, some animations could be slower, especially when you have a lot of
concurrent animations.
Another way we can obtain the same effect as force-feeding is by using the begin option,
like so:
Velocity(el,
{ opacity: 1 },
{ begin: () => { el.style.opacity = 0 } }
)
[ 393 ]
Transitions and Animations
Chapter 13
This will set the opacity to zero just before the animation begins (and hence, before the
element is inserted). This will help in slower browsers in which forcefeeding will still
display a flash of the car before bringing it all the way to the right and starting the
animation.
The possible hooks for JavaScript animations are summarized in this table:
Attribute
Description
@before-enter
@enter
@after-enter
This function is called before the element is inserted.
This function is called when the element is inserted.
This function is called when the element is inserted and the animation is finished.
This function is called when the animation is still in progress, but the element has
@enter-cancelled to leave. If you use Velocity you can do something like Velocity(el,
"stop").
@before-leave
This function is called before the leave function is triggered.
@leave
This function is called when the element leaves.
@after-leave
This function is called when the element leaves the page.
@leave-cancelled This is called in case the element has to be inserted before the leave call is
finished. It works only with v-show.
Just be reminded that these hooks are valid for any library, not just
Velocity.
There's more...
We can try another take with this interface by implementing a cancel button. If the user
booked a cab by mistake, hitting cancel will delete the reservation, and it will be apparent
by the fact that the little taxi emoji disappears.
First, let's add a cancel button:
Cancel
[ 394 ]
Transitions and Animations
Chapter 13
That was easy enough; now we add our leave transition:
That brings us to our leave method:
leave (el) {
Velocity(el,
{ opacity: [0, 1], 'font-size': ['0.1em', '1em'] },
{ duration: 200})
}
What we are doing is making the emoji disappear while scaling it down.
If you try to run your code, you will encounter some problems.
When you click on the cancel button, what should happen is the leave animation should
start and the taxi should become smaller and eventually disappear. Instead, nothing
happens and the taxi disappears abruptly.
The reason the cancel animation doesn't play as planned is because since the animation is
written in JavaScript instead of CSS, Vue has no way to tell when the animation is finished.
In particular, what happens is that Vue thinks that the leave animation is finished before it
even starts. That is what makes our car disappear.
The trick lies in the second argument. Every hook calls a function with two arguments. We
have already seen the first, el, which is the subject of the animation. The second is a
callback that when called, tells Vue that the animation is finished.
We will leverage the fact that Velocity has an option called complete, which expects a
function to call when the animation (from the Velocity perspective) is complete.
Let's rewrite our code with this new information:
leave (el, done) {
Velocity(el,
{ opacity: [0, 1], 'font-size': ['0.1em', '1em'] },
{ duration: 200 })
}
[ 395 ]
Transitions and Animations
Chapter 13
Adding the done arguments to our function lets Vue know that we want a callback to call
when the animation is finished. We don't need to explicitly use the callback as Vue will
figure it out by itself, but since it's always a bad idea to rely on default behaviors (they can
change if they are not documented), let's call the done function when the animation is
finished:
leave (el, done) {
Velocity(el,
{ opacity: [0, 1], 'font-size': ['0.1em', '1em'] },
{ duration: 200, complete: done })
}
Run your code and press the Cancel button to cancel your taxi!
Transitioning on the initial render
With the appear keyword, we are able to package transition for elements when they are
first loaded. This helps the user experience in that it gives the impression that the page is
more responsive and faster to load when you apply it to many elements.
Getting ready
This recipe doesn't assume any particular knowledge, but if you have completed at least
the Adding some fun to your app with CSS transitions recipe, it will be a piece of cake.
How to do it...
We will build a page about the American actor Fill Murray; no, not Bill Murray. You can
find more information about him at http://www.fillmurray.com. We will use images from
this site to fill our page about him.
In our HTML, let's write a header as the title of our page:
The Fill Murray Page
After the title, we will place our Vue application:
[ 396 ]
Transitions and Animations
Chapter 13
The internet was missing the ability to
provide custom-sized placeholder images of Bill Murray.
Now it can.
Which when rendered in a browser would appear like the following:
Our page is very plain right now. We want the Fill Murray picture to fade in. We have to
wrap it inside a transition:
The following are the CSS classes:
img {
float: left;
padding: 5px
}
.v-enter {
opacity: 0
}
.v-enter-active {
transition: opacity 2s
}
Running our page now will make the image appear slowly, but it will also move the text.
To fix it, we have to specify the image size in advance:
This way, our browser will set aside some space for the image that will appear slowly.
[ 397 ]
Transitions and Animations
Chapter 13
How it works...
The appear directive in the transition tag will make the components appear for the first
time with an associated transition (if it is found).
There are many possible ways to specify a transition on the first rendering of the
component. In all cases, the appear directive must be specified.
The first things Vue will look for when this directive is present are JavaScript hooks or CSS
classes specified in the tag:
My element
After that, if a name is specified, Vue will look for an entrance transition for that element:
My element
The preceding code will look for classes named as follows:
.myTransition-enter {...}
.myTransition-enter-active {...}
Vue will look for the default CSS classes for the element insertion (v-enter and v-enteractive) if everything else fails. Incidentally, this is what we have done in our recipe.
Relying on these defaults is not a good practice; here, we have done it just
as a demonstration. You should always give names to your transitions.
[ 398 ]
Transitions and Animations
Chapter 13
Maybe it's worth mentioning why we had to add the width and height to our image. The
reason is that when we specify an image URL in our HTML, the browser doesn't know the
size of the image in advance, so it doesn't reserve any space for it by default. Only by
specifying the size of the image in advance, the browser is able to correctly compose the
page even before an image is loaded.
Transitioning between elements
Everything on a web page is an element. You can easily make them appear and disappear,
thanks to Vue v-if and v-show directives. With transitions, you can easily control how
they appear and even add magic effects. This recipe explains how to do it.
Getting ready
For this recipe, you should have some familiarity with Vue transitions and how CSS works.
How to do it...
Since we talked about magic, we will turn a frog into a princess. The transformation itself
will be a transition.
We will instantiate a button that, when pressed, will represent a kiss to the frog:
Kiss!
Every time the button is pressed, the variable kisses increases. The variable will be
initialized to zero, as the following code shows:
new Vue({
el: '#app',
data: {
kisses: 0
}
})
[ 399 ]
Transitions and Animations
Chapter 13
Next, we need the frog and the princess that we will add immediately after the button:
frog
princess
The fade transition is the following CSS:
.fade-enter-active, .fade-leave-active {
transition: opacity .5s
}
.fade-enter, .fade-leave-active {
opacity: 0
}
To make it work properly, we need a last CSS selector to add:
p {
margin: 0;
position: absolute;
font-size: 3em;
}
If you run the application and click enough times the kiss button, you should see your frog
turn into a princess:
This transition will have a fade effect:
[ 400 ]
Transitions and Animations
Chapter 13
The frog emoji will turn into a princess emoji:
How it works...
When we wrote the two elements, we used the key attribute specifying who is the frog and
who is the princess. This is because, Vue optimization system will kick in otherwise. It will
see that the content of the two elements can be swapped without swapping the elements
themselves and no transition will ensue since the element was the same and only the
content changed.
If we remove the key attribute, we can see for ourselves that the frog and the princess will
change, but without any transition:
frog
princess
Consider that we use two different elements, as shown:
frog
princess
Also, we modify our CSS selector for accordingly:
p, span {
margin: 0;
position: absolute;
font-size: 3em;
display: block;
}
[ 401 ]
Transitions and Animations
Chapter 13
Now if we launch our application again, everything works without using any key attribute.
Using keys is generally recommended even when not necessary, like in
the preceding case. This is especially true when items have a different
semantic meaning. There are a couple of reasons for this. The main reason
is that when multiple people work on the same line of code, modifying the
key attribute will not break the application as easily as switching a span
element back into a p element, which will ruin the transition as we just
saw.
There's more...
Here, we cover two subcases of the preceding recipe: switching between more than two
elements and binding the key attribute.
Transitioning between more than two elements
We can expand on the recipe we just completed in a straightforward manner.
Let's suppose that if we kiss the princess too many times, she will turn into Santa Claus,
which may or may not be appealing, depending on your age I guess.
First, we add the third element:
frog
santa
princess
We can launch the application immediately and when we kiss the princess/frog more than
five times, Santa will appear with the same fading transition:
[ 402 ]
Transitions and Animations
Chapter 13
Using this setup, we are limited in using the same transition we used between the first two
elements.
There is a workaround for this explained in the Dynamic transitions recipe.
Setting the key attribute dynamically
We don't have to write the key for all our elements if we already have some data available.
Another way we could write the same app, but without repeating the element is as follows:
{{emoji}}{{transformation}}
This, of course, means that we have to provide a sensible value for the transformation
and emoji variables, depending on the number of kisses.
To do this, we will tie them to computed properties:
computed: {
transformation () {
if (this.kisses < 3) {
return 'frog'
}
if (this.kisses >= 3 && this.kisses <= 5) {
return 'princess'
}
if (this.kisses > 5) {
return 'santa'
}
},
emoji () {
switch (this.transformation) {
case 'frog': return ' '
case 'princess': return ' '
case 'santa': return ' '
}
}
}
We traded off some complexity in the template for some more logic in our Vue instance.
This can be good in the long run if we expect more complex logic in the future or if the
number of transformation rises.
[ 403 ]
Transitions and Animations
Chapter 13
Letting an element leave before the enter
phase in a transition
In the Transitioning between elements recipe, we explored how to make the transition
between two elements. The default behavior of Vue is to start the transition of the element
that is entering at the same time that the first element is leaving; this is not always
desirable.
You will learn about this important corner case and how to work around it in this recipe.
Getting ready
This recipe builds on top of the transitioning between two elements and solves a specific
problem. If you don't know what we are talking about, go back one recipe and you'll be on
track in no time.
How to do it...
First, you will see the problem if you have not encountered it yet. Next, we'll see what Vue
offers us to solve it.
The two elements problem
Let's create a carousel effect on our website. The user will view one product at a time and
then he will swipe to the next product. To swipe to the next product the user will need to
click a button.
First, we need our list of products in the Vue instance:
new Vue({
el: '#app',
data: {
product: 0,
products: [' umbrella', ' computer', '
}
})
[ 404 ]
ball', '
camera']
Transitions and Animations
Chapter 13
In our HTML, we will only need a button and the view of an element:
next
{{products[product % 4]}}
The modulo 4 (product % 4) is only because we want to start all over again when the list of
products finishes.
To set up our sliding transition, we will need the following rules:
.slide-enter-active, .slide-leave-active {
transition: transform .5s
}
.slide-enter {
transform: translateX(300px)
}
.slide-leave-active {
transform: translateX(-300px);
}
Also, to make everything look good, we finish up with the following:
p {
position: absolute;
margin: 0;
font-size: 3em;
}
If you run the code now, you will see a nice carousel:
Now, let's try to remove the position: absolute from the last rule:
p {
margin: 0;
font-size: 3em;
}
[ 405 ]
Transitions and Animations
Chapter 13
If you try your code now, you will see a weird jumping from the products:
This is the problem we are trying to solve. The second transition starts before the first
product has left. If the positioning is not absolute, we will see some weird effects.
Transition modes
To fix this problem, we will change the transition mode. Let's modify the
code:
{{products[product%4]}}
Now run your program and you will see the products taking a little more time before
sliding inside the screen. They are waiting for the previous item to go away before entering.
How it works...
To recapitulate, you have two different ways to manage transitions between components in
Vue. The default way is to start the in transition at the same time with the out transition. We
can make that explicit with the following:
We can change this default behavior by waiting for the out part to be finished before
starting the in animation. We achieved it with the following:
[ 406 ]
Transitions and Animations
Chapter 13
While the former is useful when elements have the absolute style position, the latter is more
relevant when we really need to wait to have a clear way before putting more stuff on the
page.
Absolute positioning won't care about having elements on top of each other because they
don't follow the flow of the page. On the other hand, static positioning will append the
second element after the first, making the transition awkward if both the elements are
shown at the same time.
Adding entering and leaving transitions for
elements of a list
Here, we will try to add a visual way to suggest that an element is added or removed from
the list. This can add a lot to UX since you have an opportunity to suggest to the user why
an element was added or removed.
Getting ready
Some familiarity with CSS and transition will help. If you feel like this is needed, just
browse the other recipes in this chapter.
How to do it...
We'll build a syllabus to study programming. When we are done with a topic, we'll feel
relieved and we want to incorporate that feeling in our app by making the topic float away
from the syllabus as we learn it.
The data of the list will be in our Vue instance:
new Vue({
el: '#app',
data: {
syllabus: [
'HTML',
'CSS',
'Scratch',
'JavaScript',
'Python'
]
}
[ 407 ]
Transitions and Animations
Chapter 13
})
The list will be printed in our HTML with the following code:
When we press a button, we want the topic to disappear from the list. For this to happen,
we need to modify the code we have written.
First, let's add a Done button before each topic:
Done {{topic}}
Here, the completed method will look like this:
methods: {
completed (topic) {
let index = this.syllabus.indexOf(topic)
this.syllabus.splice(index, 1)
}
}
Running the code now will reveal a simple application for checking off the topics we
already studied. What we want though is an animation that will make us feel relieved.
For that, we need to edit the container of our list. We remove the tag and, instead, tell
the to compile to a tag:
Done {{topic}}
[ 408 ]
Transitions and Animations
Chapter 13
Note that we also added a key to each list element according to the topic. The last thing we
need is adding the transition rules to our CSS:
.v-leave-active {
transition: all 1s;
opacity: 0;
transform: translateY(-30px);
}
Now, the subjects will disappear with the transition on clicking the Done button, as shown:
How it works...
The tag represents a container for a group of elements that will be
displayed at the same time. By default, it represents the tag, but by setting the tag
attribute to ul, we made it represent an unordered list.
Every element in the list must have a unique key or the transitions won't work. Vue will
take care of applying a transition to every element that enters or leaves.
Transitioning elements that move in a list
In this recipe, you will build a list of elements that move according to how the list changes.
This particular animation is useful when you want to tell your user that something has
changed and the list is now updated accordingly. It will also help the user identify the point
in which the element was inserted.
[ 409 ]
Transitions and Animations
Chapter 13
Getting ready
This recipe is a little advanced; I would suggest you complete some of the recipes in this
chapter if you are not very familiar with transitions in Vue. If you can complete the Adding
entering and leaving transitions for elements of a lists recipe without much difficulty, you are
good to go.
How to do it...
You will build a little game--a bus station simulator!
Whenever a bus--represented by its emoji--leaves the station, all the other buses will drive a
little ahead to take its place. Every bus is identified by a number, as you can see from the
Vue instance data:
new Vue({
el: '#app',
data: {
buses: [1,2,3,4,5],
nextBus: 6
}
})
Whenever a new bus arrives, it will have a progressive number assigned. We want a new
bus to leave or go every two seconds. We can achieve this by hooking a timer when our
component is mounted on screen. Immediately after data, write the following:
mounted () {
setInterval(() => {
const headOrTail = () => Math.random() > 0.5
if (headOrTail()) {
this.buses.push(this.nextBus)
this.nextBus += 1
} else {
this.buses.splice(0, 1)
}
}, 2000)
}
The HTML of our app will look like this:
Bus station simulator
[ 410 ]
Transitions and Animations
Chapter 13
To make the buses move around, we need to specify some CSS rules under the prefix
station:
.station-leave-active, .station-enter-active {
transition: all 2s;
position: absolute;
}
.station-leave-to {
opacity: 0;
transform: translateX(-30px);
}
.station-enter {
opacity: 0;
transform: translateX(30px);
}
.station-move {
transition: 2s;
}
span {
display: inline-block;
margin: 3px;
}
Launching the app now will result in an orderly queue of buses in which one leaves or
arrives every two seconds:
[ 411 ]
Transitions and Animations
Chapter 13
How it works...
The core of our app is the tag. It manages all the buses identified by
their key:
Whenever a bus enters or leaves the scenes, a FLIP animation (see the Adding your own
transition classes recipe) will be automatically triggered by Vue.
To fix ideas, let's say we have buses [1, 2, 3] and bus 1 leaves the scene. What happens next
is that the properties of the first bus's element will be memorized before the
animation actually starts. So we may retrieve the following object describing the properties:
{
bottom:110.4375
height:26
left:11
right:27
top:84.4375
width:16
}
Vue does this for all the elements keyed inside the tag.
After this, the station-leave-active class will be applied to the first bus. Let's briefly
review what the rules are:
.station-leave-active, .station-enter-active {
transition: all 2s;
position: absolute;
}
We note that the position becomes absolute. This means that the element is removed from
the normal flow of the page. This in turn means that all the buses behind him will suddenly
move to fill the space left blank. Vue records all the properties of the buses at this stage also
and this is considered the final frame of the animation. This frame is not actually a real
displayed frame; it is just used as an abstraction to calculate the final position of the
elements:
[ 412 ]
Transitions and Animations
Chapter 13
Vue will calculate the difference between the final frame and the starting frame and will
apply styles to make the buses appear in the initial frame even if they are not. The styles
will be removed after one frame. The reason the buses slowly crawl to their final frame
position instead of immediately moving in their new position is that they are span
elements and we specified that any transform style (the one used by Vue to fake their
position for one frame) must be transitioned for two seconds:
.station-move {
transition: 2s;
}
In other words, at frame -1, the three buses are all in place and their position is recorded.
At frame 0, the first bus is removed from the flow of the page and the other buses are
instantaneously moved behind it. In the very same frame, Vue records their new position
and applies a transform that will move the buses back to where they were at frame -1
giving the appearance that nobody moved.
At frame 1, the transform is removed, but since we have a transition, the buses will slowly
move to their final position.
Animating the state of your components
In computers, everything is a number. In Vue, everything that is a number can be animated
in one way or other. In this recipe, you will control a bouncy ball that will smoothly
position itself with a tween animation.
[ 413 ]
Transitions and Animations
Chapter 13
Getting ready
To complete this recipe, you will need at least some familiarity with JavaScript. The
technicalities of JavaScript are out of the scope of this book, but I will break the code down
for you in the How it works... section, so don't worry too much about it.
How to do it...
In our HTML, we will add only two elements: an input box in which we will enter the
desired position of our bouncy ball and the ball itself:
To properly render the ball, write this CSS rule and it will appear on the screen:
.ball {
width: 3em;
height: 3em;
background-color: red;
border-radius: 50%;
position: absolute;
left: 10em;
}
We want to control the bar Y position. To do that, we will bind the top property of the ball:
Height will be part of our Vue instance model:
new Vue({
el: '#app',
data: {
height: 0
}
})
[ 414 ]
Transitions and Animations
Chapter 13
Now, since we want the ball to animate in the new position whenever the enteredHeight
changes, one idea would be to bind the @change event of the input element:
The move method will be the one responsible for taking the current height of the ball and
slowly transitioning it to the specified value.
Before doing this, you will add the Tween.js library as a dependency. The official
repository is at https://github.com/tweenjs/tween.js. You can add the CDN link
specified in the README.md page if you are using JSFiddle.
Add the move method after adding the library, like this:
methods: {
move (event) {
const newHeight = Number(event.target.value)
const _this = this
const animate = (time) => {
requestAnimationFrame(animate)
TWEEN.update(time)
}
new TWEEN.Tween({ H: this.height })
.easing(TWEEN.Easing.Bounce.Out)
.to({ H: newHeight }, 1000)
.onUpdate(function () {
_this.height = this.H
})
.start()
animate()
}
}
Try to launch the app and see the ball bounce while you edit its height:
[ 415 ]
Transitions and Animations
Chapter 13
When we change the height, the position of the ball also changes:
How it works...
The general principle here is that you have a state for an element or component. When the
state is numeric in nature, you can "tween" (from between) from one value to the other
following a specific curve or acceleration.
Let's break down the code, shall we?
The first thing we do is to take the specified new height for the ball and save it to the
newHeight variable:
const newHeight = Number(event.target.value)
In the next line, we are also saving our Vue instance in a _this helper variable:
const _this = this
The reason we do so will be clear in a minute:
const animate = (time) => {
requestAnimationFrame(animate)
TWEEN.update(time)
}
In the preceding code, we are wrapping all of our animation in a function. This is idiomatic
to the Tween.js library and identifies the main loop we will use to animate. If we have other
tweens, this is the place to trigger them:
new TWEEN.Tween({ H: this.height })
.easing(TWEEN.Easing.Bounce.Out)
.to({ H: newHeight }, 1000)
.onUpdate(function () {
_this.height = this.H
})
.start()
[ 416 ]
Transitions and Animations
Chapter 13
This is the API call to our library. First, we are creating an object that will hold a copy of the
height value in lieu of our component. Normally, here you put the object that represents the
state itself. Due to Vue limitations (or Tween.js limitations), we are using a different
strategy; we are animating a copy of the state and we are syncing the true state for every
frame:
Tween({ H: this.height })
The first line initializes this copy to be equal to the current actual height of the ball:
easing(TWEEN.Easing.Bounce.Out)
We choose the easing to resemble a bouncy ball:
.to({ H: newHeight }, 1000)
This line sets the target height and the number of milliseconds the animation should last
for:
onUpdate(function () {
_this.height = this.H
})
Here, we are copying the height of the animation back to the real thing. As this function
binds this to the copied state, we are forced to use ES5 syntax to have access to it. This is
why we had a variable ready to reference the Vue instance. Had we used the ES6 syntax,
we would not have any way to get the value of H directly.
Packaging reusable transitions into
components
We may have a significant transition in our website that we want to reuse throughout the
user funnel. Packaging transition into components can be a good strategy if you are trying
to keep your code organized. In this recipe, you will build a simple transition component.
[ 417 ]
Transitions and Animations
Chapter 13
Getting ready
Following this recipe makes sense if you have already worked your way through the
transition with Vue. Also, since we are working with components, you should at least have
an idea of what they are. Skim through the next chapter for a primer on components. In
particular, we will create a functional component, the anatomy of which is detailed in
the Creating a functional component recipe.
How to do it...
We will build a signature transition for a news portal. Actually, we will use a premade
transition in the excellent magic library (https://github.com/miniMAC/magic), so you
should add it to your project as a dependency. You can find the CDN link at
https://cdnjs.com/libraries/magic (go to the page to find the link, don't copy
it as a link).
First, you will build the website page, then you will build the transition itself. Lastly, you
will just add the transition to different elements.
Building the basic web page
Our web page will consist of two buttons each that will display a card: one is a recipe and
the other is the last breaking news:
Recipe
Breaking News
Apple Pie Recipe
Ingredients: apple pie. Procedure: serve hot.
Breaking news
[ 418 ]
Transitions and Animations
Chapter 13
Donald Duck is the new president of the USA.
The cards will have their unique touch, thanks to the following CSS rule:
.card {
position: relative;
background-color: FloralWhite;
width: 9em;
height: 9em;
margin: 0.5em;
padding: 0.5em;
font-family: sans-serif;
box-shadow: 0px 0px 10px 2px rgba(0,0,0,0.3);
}
The JavaScript part will be a very simple Vue instance:
new Vue({
el: '#app',
data: {
showRecipe: false,
showNews: false
}
})
Running this code will already display your web page:
[ 419 ]
Transitions and Animations
Chapter 13
Building the reusable transition
We decided that our website will feature a transition whenever a card is displayed. Since
we intend to reuse the animation with everything in our website, we'd better package it in a
component.
Before the Vue instance, we declare the following component:
Vue.component('puff', {
functional: true,
render: function (createElement, context) {
var data = {
props: {
'enter-active-class': 'magictime puffIn',
'leave-active-class': 'magictime puffOut'
}
}
return createElement('transition', data, context.children)
}
})
The puffIn and puffOut animations are defined in magic.css.
Using our transition with the elements in our page
Now, we will just edit our web page to add the component to our cards:
Recipe
Breaking News
Apple Pie Recipe
Ingredients: apple pie. Procedure: serve hot.
[ 420 ]
Transitions and Animations
Chapter 13
Breaking news
Donald Duck is the new president of the USA.
The cards will now appear and disappear when pressing the button with a "puff" effect.
How it works...
The only tricky part in our code is building the component. Once we have that in
place, whatever we put inside will appear and disappear according to our transition. In our
example, we used an already made transition. In the real world, we may craft a seriously
complex animation that can be difficult to apply every time in the same manner. Having it
packaged in a component is much easier and maintainable.
Two things make the component work as a reusable transition:
props: {
'enter-active-class': 'magictime puffIn',
'leave-active-class': 'magictime puffOut'
}
Here, we specify the classes the component must adopt when entering and leaving; there is
nothing too special here, we have already done it in the Integrating with third-party CSS
animation libraries such as animate.css recipe.
At the end we return the actual element:
return createElement('transition', data, context.children)
This line creates the root of our element that is a tag with only one child-context.children. This means that the child is unspecified; the component will put as
child whatever actual child is passed in the template. In our examples, we passed some
cards that were promptly displayed.
[ 421 ]
Transitions and Animations
Chapter 13
Dynamic transitions
In Vue, a constant theme is reactivity and, of course, transitions can be dynamic because of
that. Not only the transition themselves, but all their properties can be bound to reactive
variables. This gives us a lot of control over which transition to use at any given moment.
Getting ready
This recipe builds on top of the Transitioning between elements recipe. You don't need to go
back if you already know about transitions, but if you feel like you're missing something, it
might be a good idea to complete that first.
How to do it...
We will transform a frog into a princess with some kisses, but if we kiss too much the
princess will turn into Santa. Of course, we are talking about emojis.
Our HTML setup is very simple:
Kiss!
{{emoji}}{{transformation}}
Just note that most of the attributes here are bound to variables. Here is how the JavaScript
unfolds.
First, we will create a simple Vue instance with all of our data:
new Vue({
el: '#app',
data: {
kisses: 0,
kindOfTransformation: 'fade',
transformationMode: 'in-out'
}
})
[ 422 ]
Transitions and Animations
Chapter 13
The fade transformation we are referring to is the following CSS:
.fade-enter-active, .fade-leave-active {
transition: opacity .5s
}
.fade-enter, .fade-leave-active {
opacity: 0
}
The variables transformation and emoji are defined by two computed properties:
computed: {
transformation () {
if (this.kisses < 3) {
return 'frog'
}
if (this.kisses >= 3 && this.kisses <= 5) {
return 'princess'
}
if (this.kisses > 5) {
return 'santa'
}
},
emoji () {
switch (this.transformation) {
case 'frog': return ' '
case 'princess': return ' '
case 'santa': return ' '
}
}
}
While we are using the fade transition between the frog and the princess, we want
something else between the princess and the frog. We will use the following transition
classes:
.zoom-leave-active, .zoom-enter-active {
transition: transform .5s;
}
.zoom-leave-active, .zoom-enter {
transform: scale(0)
}
[ 423 ]
Transitions and Animations
Chapter 13
Now, since we bound the name of the transition to a variable, we can easily switch that
programmatically. We can do that by adding the following highlighted lines to the
computed property:
transformation () {
if (this.kisses < 3) {
return 'frog'
}
if (this.kisses >= 3 && this.kisses <= 5) {
this.transformationMode = 'out-in'
return 'princess'
}
if (this.kisses > 5) {
this.kindOfTransformation = 'zoom'
return 'santa'
}
}
The first added line is to avoid having an overlap while the zoom transition starts (more on
that in the Letting an element leave before the enter phase in a transition recipe).
The second added line switches the animation to "zoom".
To make everything appear the right way, we need one more CSS rule:
p {
margin: 0;
position: absolute;
font-size: 3em;
}
This is much nicer.
Now run the app and see how the two different transitions are used dynamically:
[ 424 ]
Transitions and Animations
Chapter 13
As the number of kisses increase, the princess zooms out:
With this, the Santa zooms in:
How it works...
If you understand how reactivity works in Vue, there is not much to add. We bound the
name of the transition to the kindOfTransformation variable and switched from fade to
zoom in our code. We also demonstrated that the other attributes of the tag
can be changed on the fly as well.
[ 425 ]
14
Vue Communicates with the
Internet
In this chapter, the following recipes will be covered:
Sending basic AJAX request with Axios
Validating user data before sending it
Creating a form and sending data to your server
Recovering from an error during a request
Creating a REST client (and server!)
Implementing infinite scrolling
Processing a request before sending it out
Preventing XSS attacks to your app
Introduction
Web applications rarely work all by themselves. What makes them interesting is actually
the fact that they enable us to communicate with the world in innovative ways that didn't
exist just a few years ago.
Vue, by itself, doesn't contain any mechanism or library to make AJAX requests or open
web sockets. In this chapter, we will, therefore, explore how Vue interacts with built-in
mechanisms and external libraries to connect to external services.
You will start by making basic AJAX requests with the help of an external library. Then,
you'll explore some common patterns with sending and getting data in forms. Finally, there
are some recipes with real-world applications and how to build a RESTful client.
Vue Communicates with the Internet
Chapter 14
Sending basic AJAX requests with Axios
Axios is the recommended library for Vue for making HTTP requests. It's a very simple
library, but it has some built-in features that help you in carrying out common operations.
It implements a REST pattern for making requests with HTTP verbs and can also deal with
concurrency (spawning multiple requests at the same time) in a function call. You can find
more information at https://github.com/mzabriskie/axios.
Getting ready
For this recipe, you don't need any particular knowledge of Vue. We will use Axios, which
itself uses JavaScript promises. If you have never heard of promises, you can have a primer
at https://developers.google.com/web/fundamentals/getting-started/primers/
promises.
How to do it...
You will build a simple application that gives you a wise piece of advice every time you
visit the web page.
The first thing you will need is to install Axios in your application. If you are using npm,
you can just issue the following command:
npm install axios
If you are working on a single page, you can import the following file from CDN,
at https://unpkg.com/axios/dist/axios.js.
Unfortunately, the advise slip service we will use will not work with
JSFiddle because while the service runs on HTTP, JSFiddle is on HTTPS
and your browser will most likely complain. You can run this recipe on a
local HTML file.
Our HTML looks like this:
Advice of the day
{{advice}}
[ 427 ]
Vue Communicates with the Internet
Chapter 14
Our Vue instance is as follows:
new Vue({
el: '#app',
data: {
advice: 'loading...'
},
created () {
axios.get('http://api.adviceslip.com/advice')
.then(response => {
this.advice = response.data.slip.advice
})
.catch(error => {
this.advice = 'There was an error: ' + error.message
})
}
})
Open your app to have a refreshingly wise slip of advice:
How it works...
When our application starts up, the created hook is engaged and will run the code with
Axios. The first line performs a GET request to the API endpoint:
axios.get('http://api.adviceslip.com/advice')
[ 428 ]
Vue Communicates with the Internet
Chapter 14
This will return a promise. We can use the then method on any promise to act on the result
if the promise resolves successfully:
.then(response => {
this.advice = response.data.slip.advice
})
The response object will contain some data about the result of our request. A possible
response object is the following:
{
"data": {
"slip": {
"advice": "Repeat people's name when you meet them.",
"slip_id": "132"
}
},
"status": 200,
"statusText": "OK",
"headers": {
"content-type": "text/html; charset=UTF-8",
"cache-control": "max-age=0, no-cache"
},
"config": {
"transformRequest": {},
"transformResponse": {},
"timeout": 0,
"xsrfCookieName": "XSRF-TOKEN",
"xsrfHeaderName": "X-XSRF-TOKEN",
"maxContentLength": -1,
"headers": {
"Accept": "application/json, text/plain, */*"
},
"method": "get",
"url": "http://api.adviceslip.com/advice"
},
"request": {}
}
We navigate to the property we want to interact with; in our case, we want
response.data.slip.advice, which is the string. We copied the string in the variable
advice in the instance state.
[ 429 ]
Vue Communicates with the Internet
Chapter 14
The last part is when something wrong happens to our request or to our code inside the
first branch:
.catch(error => {
this.advice = 'There was an error: ' + error.message
})
We will explore error handling more in depth in the Recovering from an error during a
request recipe. For now, let's trigger an error by hand, just to see what happens.
The cheapest way to trigger an error is to run the app on JSFiddle. Since the browser detects
JSFiddle on a secure connection and our API is on HTTP (which is not secure), modern
browsers will complain and will block the connection. You should see the following text:
There was an error: Network Error
This is just one of the many possible errors you can experiment with. Consider that you edit
the GET endpoint to some non-existent page:
axios.get('http://api.adviceslip.com/non-existent-page')
In this case, you will get a 404 error:
There was an error: Request failed with status code 404
Interestingly, you will end up in the error branch even if the request goes well but there is
an error in the first branch.
Change the then branch to this:
.then(response => {
this.advice = undefined.hello
})
As everybody knows, JavaScript cannot read "hello" property of an undefined object:
There was an error: Cannot read property 'hello' of undefined
It's just as I told you.
[ 430 ]
Vue Communicates with the Internet
Chapter 14
Validating user data before sending it
Generally, users hate forms. While we can't do much to change that, we can make it less
frustrating for them by providing relevant instructions on how to fill them in. In this recipe,
we will create a form, and we will leverage HTML standards to provide the user with a nice
guidance on how to complete it.
Getting ready
This recipe does not need previous knowledge to be completed. While we will build a form
(the Sending basic AJAX requests with Axios recipe), we will fake the AJAX
call and concentrate on the validation.
How to do it...
We will build a very simple form: one field for the username and one for the user e-mail,
plus one button to submit the information.
Type in this HTML:
The Vue instance is trivial, as shown:
new Vue({
el: '#app',
methods: {
vueSubmit() {
[ 431 ]
Vue Communicates with the Internet
Chapter 14
console.info('fake AJAX request')
}
}
})
Run this app and try to submit the form with an empty field or wrong e-mail. You should
see help from the browser itself:
Then, if you try to enter an invalid e-mail address, you will see the following:
How it works...
We are using a native HTML5 validation API, which internally uses pattern matching to
check whether what we are typing is conformant to certain rules.
Consider the attribute required in the following line:
This ensures that when we submit the form, the field is actually populated while having
type="email" in the other input element ensures that the content resembles an e-mail
format.
This API is very rich and you can read more at
https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Forms/Data_form_validat
ion.
[ 432 ]
Vue Communicates with the Internet
Chapter 14
Many a time, the problem is that to leverage this API, we need to trigger the native
validation mechanism. This means that we are not allowed to prevent the default behavior
of the Submit button:
Submit
This will not trigger the native validation and the form will always be submitted. On the
other hand, if we do the following:
Submit
The form will get validated but, since we are not preventing the default behavior of the
submit button, the form will be sent to another page, which will destroy the one-page
application experience.
The trick is to intercept the submit at form level: