This project is meant to teach Bootstrap fundamentals by creating a bookshop, step by step.
It uses Bootstrap 5 pages, Handlebars templating, some JavaScript and the Bookly E-Commerce theme, and it stores its data in the browser's LocalStorage. Please try the live-demo on: https://artingo.github.io/Bootstrap-BookShop/
This is how the final shop looks like:

Follow these steps to implement the bookshop:
-
Search for an adequate theme, download it and copy the files into this project. For instance, I found my E-Commerce shop theme on TemplatesJungle.
-
Download any CDN resources, copy them to your local folders and adjust their references.
<link rel="stylesheet" href="css/bootstrap.min.css"> <link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/swiper-bundle.min.css" /> ... <script src="js/vendors/jquery-1.11.0.min.js"></script> <script src="js/vendors/bootstrap.bundle.min.js"></script> <script src="js/vendors/swiper-bundle.min.js"></script> <script src="js/vendors/script.js"></script>
-
Open the start page (index.html) in your browser. In a JetBrains IDE, the URL would be e.g. http://localhost:63342/BookShop/index.html
-
Check and correct all hyperlinks so that the navigation works, locally, and no network or JavaScript errors occur.
-
Download Handlebars.js from the official website and copy it to the vendors folder.
-
Download my vanilla.js file from GitHub and copy it to the js folder. It contains basic DOM, Events and Render functions.
-
In the HTML
headsection add 2 `script tags:<script src="js/vendors/handlebars.min.js"></script> <script src="js/vanilla.js"></script>
-
Create a partials folder for header, footer and other templates.
-
Inside, create an empty header.html file.
-
Edit the index.html file, cut out it's header code and paste it into the header.html file.
<svg xmlns="http://www.w3.org/2000/svg">... <div id="preloader" class="preloader-container">... <div class="search-popup">... <header id="header" class="site-header">...
-
Now, we want to replace the former static
headercode with the newly created Handlebars template. To do so, replace the formerheaderwith this code:<div> <script type="text/x-handlebars-template"> {{> partials/header index='active' }} </script> </div>
-
To active Handlebars' template rendering, add a
rendercall to the HTML body:<body onload="render()">. This function will scan your HTML page for any Handlebars<script>tags and dynamically render them. It will also load any partials. -
Reload the index page in your browser, open the JavaScript console by pressing the
F12key, and correct any JS errors that may occur. -
The last step is to dynamically highlight the active navigation link. To do so, add this small Handlebars code to the Overview navigation link in header.html:
Add a corresponding Handlebar code to all navigation links in header.html.
The corresponding part in index.html looks like this: -
To replace all headers with a Handlebar template, repeat steps 7 to 10 for all HTML files.
- Inside the partials folder, create an empty hero.html file.
- Move the HTML code from the hero section to this new file.
- At the
<h1>tag, replace the static title with a Handlebars expression:<h1>{{title}}</h1> - In the index.html file, place a Handlebars script and pass the title as parameter:
- Repeat steps 1 to 4 for the
<footer>section.
- Repeat steps 1 to 4 for the
<head>section.
In all other HTML files, replace the static HTML sections with dynamic Handlebars scripts.
To keep the store flexible, it is important to be able to load the books from outside. To do this, we need to load the book data from an external file. We use the "JSON" format, which is very common and can be exported from many data sources.
- In the js folder, create a data.js file that contains the book data. It may look like this:
const data = { "books": [ { "id": 1, "title": "20000 Leagues Under the Sea", "author": "Verne, Jules", "genre": "fiction", "publisher": "Wordsworth", "price": 19.7, "image": "https://m.media-amazon.com/images/I/913ii-IVEXL._AC_UY218_.jpg" }, ... ] }
- In the index.html file, add a
<script>tag to load that data file.<head> <script src="js/vendors/handlebars.min.js"></script> <script src="js/vanilla.js"></script> <script src="js/data.js"></script> ... </head>
- Load the index page in the browser, open a JavaScript console by pressing CTRL + SHIFT + J (Windows / Linux) or COMMAND + OPTION + J (macOS). Type this command in the console to check the proper data loading:
console.log(data). It should prompt the books as seen on this screenshot:

- Now it is time to display the books' data on the index page. Iterate over the books with
#each books. - Within the loop you can display each field by using its name, e.g.
{{title}}. - If there is no image data, you may replace it with a placeholder image, using Handlebars'
if elsehelper. - The resulting code may look like this:
- The shop should now display all your books, like in the screenshot:

Now, we can see all 209 books from the data file but these are too many entries at once. To handle this, we introduce a pagination, i.e. show only 15 books at once and offer a navigation to the next / previous page.
- In the js folder, create an index.js JavaScript file to handle the JavaScript code for the index page. Write a function that handles the pagination of the books array:
function paginate(books) - We want to control the pagination via the URL parameter "page". So we query this parameter in our function. If no parameter is present, we use
1as our default value.const params = new URLSearchParams(location.search) const currentPage = parseInt(params.get('page')) || 1
- Next, we want to calculate the
pageCountto know how many pagination entries we need to create.const totalBooks = books.length const size = 15 const pageCount = Math.ceil(totalBooks / size)
- We use a
pagesarray to model the pagination entries. Inside, thepagecontains the number of the current pagination page, whileactivemarks the current page that needs to be highlighted.const pages = Array.from({length: pageCount}, function (value, index) { const page = index + 1 return { page: page, active: page === currentPage ? 'active' : '' } })
- We also want to display the current book list section with
from,toandtotalBooks.let from = (currentPage - 1) * size const to = from + size const totalBooks = books.length
- Next, we send this data to the
render()function within vanilla.js.render({ books: books.slice(from, to), from: ++from, to: to, totalBooks: totalBooks, currentPage: currentPage, pages: pages, prevPage: Math.max(currentPage - 1, 1), nextPage: Math.min(currentPage + 1, pageCount) })
- To show the
from,toandtotalBooksvalues, open the index file and scroll to theshowing-productsection. Insert this code: - To show the pagination links, scroll to the
Page navigationsection and insert this code: - The result should look like this:

- Test the proper pagination by clicking the links!
Next, we want to be able to sort the books by title, price, etc. On the home page, there is a dropdown list that we want to populate and activate. The best way is to keep it in the JavaScript model. Thus, we can add more sorting in the future.
-
In the index.js file, create an object to contain keys and labels of the sorting:
const SORTING = { DEFAULT: "Reset Sorting", ALPHA_UP: "Name A-Z", ALPHA_DOWN: "Name Z-A", PRICE_UP: "Price (Low-High)", PRICE_DOWN: "Price (High-Low)" }
-
Implement a method to create the model of the dropdown list. We need an array of objects containing
value, selected and label.function createSortingModel(currentSorting) { const sorting = [] for (const [key, value] of Object.entries(SORTING)) { const entry = { value: key, selected: (key === currentSorting) ? "selected" : "", label: value, } sorting.push(entry) } return sorting }
-
Now, we can populate the
<option>array of the sorting dropdown in the index.html file. -
Next, we want to implement the JavaScript function that triggers the sorting, once we change the selection in the dropdown list. For that, we pass the dropdown reference and append its value to the URL.
function triggerSorting(element) { const searchParams = new URLSearchParams(location.search) searchParams.set('sort', element.value) searchParams.set('page', "1") location.search = searchParams.toString() }
-
The last step is to actually sort the books array. In JavaScript, you can sort an array by passing a sorting function with parameters
a and b. Inside, you compare these neighboring array elements and return1,0or-1. This moves the second element up, same place or down in the list.function handleSorting(books, currentSorting) { switch (currentSorting) { case "DEFAULT": case "ALPHA_UP": books.sort(function(a, b) { return a.title < b.title ? -1 : a.title > b.title ? 1 : 0 }) break case "ALPHA_DOWN": books.sort(function(a, b) { return a.title < b.title ? 1 : a.title > b.title ? -1 : 0 }) break case "PRICE_UP": books.sort(function(a, b) { return a.price - b.price }) break case "PRICE_DOWN": books.sort(function(a, b) { return b.price - a.price }) } return books }
-
Test the sorting in the browser. For instance, if you sort by price (descending), the result should look like this:

When you have a lot of books, it makes sense to filter them by 'genres'. It works in a similar way to sorting:
- create the model for the HTML links
- react to search parameters
- handle the current genre in the books model
-
In the index.js file, create an object to contain keys and titles of the genres:
const GENRES = { comic: "Comics", computer_science: "Computer Science", ... signal_processing: "Signal Processing" }
-
Implement a method to create the model of the genres list. We need an array of objects containing
value, active and title.function createGenreModel(currentGenre) { const genres = [] for (const [key, value] of Object.entries(GENRES)) { const entry = { value: key, active: key === currentGenre? 'active' : '', title: value, } genres.push(entry) } return genres }
-
Now, we can populate the genres in the genres.html file.
-
If you want, you can also generate the genres in the search pop-up, dynamically. To do so, insert this code in the
search-popupsection of header.html: -
As the current genre is now passed as a search parameter, we can react to it.
function paginate(books) { ... const currentGenre = params.get('genre') const filteredBooks = handleGenres(books) const totalBooks = filteredBooks.length ... render({ genres: createGenreModel(currentGenre) ... }) }
-
The last step is to actually filter the books array. In JavaScript, you can filter an array by passing a function that decides whether an entry should be returned or not.
function handleGenres(books, currentGenre) { if (currentGenre) { const filteredBooks = books.filter(function(book) { return book.genre === currentGenre }) return filteredBooks } // if genre is empty, return all books return books }
-
Test the genre filtering in the browser. For instance, if you click on
history, the result should look like this:

Finally, we want to be able to search for books by title and author.
-
We want to trigger the search from the big search field above the genres.

<form name="aside-search" onsubmit="return triggerSearch(this.search.value)"> <input type="search" name="search"> <button type="submit"> <svg><use xlink:href="#search"></use></svg> </button> </form>
-
Let's implement that JavaScript function that adds the
searchTermto the URL parameters.function triggerSearch(searchTerm) { if (searchTerm) { const searchParams = new URLSearchParams(location.search) searchParams.set('search', searchTerm) location.search = searchParams.toString() } return false }
-
In
paginate()we pass thesearchTermto thehandleGenres()function.function paginate(books) { ... const searchTerm = params.get('search') const filteredBooks = handleGenres(books, currentGenre, searchTerm) ... render({ search: searchTerm, ... } }
-
In
handleGenres(), after filtering the genre, we additionally filter the books by thesearchTerm. For the comparison itself, we useincludes()because it also finds partial String matches. And to receive more results, we transform title and author to lower case.function handleGenres(books, currentGenre, searchTerm) { ... if (searchTerm) { const searchTermLower = searchTerm.toLowerCase() filteredBooks = filteredBooks.filter(function(book) { return book.title.toLowerCase().includes(searchTermLower) || book.author.toLowerCase().includes(searchTermLower) }) } }
-
To re-use the
searchTerm, we pass it to Handlebars, so it gets displayed in the search field after page reload. -
You may want to add the search functionality to the search pop-up in header.html, as well.
-
To maintain all the URL parameters, you should refactor your pagination.html to contain the additional parameters.
-
Test the combined search, genre, sorting and pagination in your browser.

Now that we can browse through the product range, it's time to fill the shopping cart. To do this, we first implement the addition of an article to the shopping cart.
As the mini shopping cart will appear in the header of every page, we outsource its JavaScript code to a separate shop.js file. First of all, we need an internal data model to map the shopping cart.:
let cart = {
items: [],
numOfItems: 0,
grandTotal: 0
}We also save its status in SessionStorage so that we can access it from any page:
function initCart(querySelector) {
const cartInSession = sessionStorage.getItem("cart")
if (cartInSession) {
cart = JSON.parse(cartInSession)
}
render(cart, querySelector)
}To update the mini shopping cart in the header, on every page, as soon as the DOM is loaded, it is loaded from the SessionStorage and rendered .
document.addEventListener("DOMContentLoaded", function (event) {
setTimeout(function () {
if (cart.numOfItems === 0) {
initCart()
}
// wait 150 milliseconds, so that other Handlebars DOM elements may be created, first!
}, 150)
})- First, we add a JavaScript click handler to the shopping cart icon.
<button type="button" onclick="addToCart({{id}})"> <svg class="cart"><use xlink:href="#cart"></use></svg> </button>
- In
addCart(), we first check if this is a valid article. Next, we check if this item already exists in the shopping cart. If not, we create acartItemwithquantityandtotalfields. After that, we add the newcartItemto the array ofcart.items. Last, we increase thequantityand refresh the header.function addToCart(articleNo) { // is this a valid shop article? const shopItem = findItemInArticles(articleNo) if (shopItem) { // is article already in cart? let cartItem = findItemInCart(articleNo) if (!cartItem) { cartItem = shopItem cartItem.quantity = 0 cartItem.total = 0.0 cart.items.push(cartItem) } increaseQuantity(cartItem) refresh('#partial-header') } else { console.warn(`Article with id '${articleNo}' doesn't exist`) } }
In increaseQuantity(), we set a couple of field to display in the mini cart, as well as the cart page.
function increaseQuantity(item, doRefresh) {
item.quantity++
item.showQuantity = (item.quantity > 1)
item.total += item.price
cart.numOfItems++
cart.grandTotal += item.price
}To persists the current cart items temporarily, we store them in the SessionStorage:
sessionStorage.setItem("cart", JSON.stringify(cart))- To show the mini cart in the header, we re-render the
#partial-headerand pass the cart data as argument.render(cart, '#partial-header')
- In header.html, we iterate over the cart items and display their details.
In cart.html, we display the cart items as a table. We add buttons to increase / decrease their quantity, and a delete icon.
- To increase and decrease the quantity of a cart item, we use buttons that trigger the corresponding JavaScript functions.
<button type="button" onclick="decreaseQuantity({{id}})"> <svg><use xlink:href="#minus"></use></svg> </button> ... <button type="button" onclick="increaseQuantity({{id}}, true)"> <svg><use xlink:href="#plus"></use></svg> </button>
- Inside
decreaseQuantity(), we modify the cart model and refresh the Handlebars templates on the page.function decreaseQuantity(item) { if (item.quantity > 0) { item.quantity-- item.total -= item.price cart.numOfItems-- cart.grandTotal -= item.price refresh('#partial-header') refresh('#cart-table') } } function refresh(querySelector) { sessionStorage.setItem("cart", JSON.stringify(cart)) render(cart, querySelector) }
- As the cart item removal is an irrevocable operation, we display a confirm dialog, before. We also memorize the id of the
itemToRemove, so we can refer to it, later.<a href="#" data-bs-toggle="modal" data-bs-target="#confirm-delete" onclick="itemToRemove={{id}}"> <svg class="cart-cross-outline" width="38" height="38"> <use xlink:href="#cart-cross-outline"></use> </svg> </a>
- The confirm dialog uses standard BootsTrap mechanisms to display and dismiss it.
<div class="modal fade" id="confirm-delete" tabindex="-1"> <div class="modal-dialog modal-sm"> <div class="modal-content"> <div class="modal-header"> <h1 class="modal-title fs-5">Confirm removal</h1> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> Do you really want to remove this cart item? </div> <div class="modal-footer"> <button type="button" class="btn btn-dark" data-bs-dismiss="modal">Cancel</button> <button type="button" class="btn btn-danger" data-bs-dismiss="modal" onclick="removeFromCart(itemToRemove)">Yes, remove!</button> </div> </div> </div> </div>
- Finally, we recalculate the cart model, remove the current item from the
cart.itemsand display the changes on the page by callingrefresh().function removeFromCart(articleNo) { const cartItem = findItemInCart(articleNo) if (cartItem) { cart.numOfItems -= cartItem.quantity cart.grandTotal -= cartItem.total const itemIndex = cart.items.indexOf(cartItem) cart.items.splice(itemIndex, 1) refresh('#partial-header') refresh('#cart-table') } }




