We will start creating pagination with building a helper function in a separate file, which we will use later on. This function will be used in our search system. Function uses Regular Expressions (RegEx) and it escapes a lot of characters. I won’t be covering Regex in this article. But, if you want to learn more about RegEx, which is a really good ‘tool’ to have under your belt, check this link.

Helper Function (regex-escape.js)

// Regex function for search functionality

const escapeRegex = (string) => {

return string.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");

}; // Exporting Function

module.exports = escapeRegex;

STEP 1 - Setting up MongoDB Product Schema (Products.js)

First of all, we will need to setup MongoDB Schema/Model for products.

Products will have data such as: Name, Description, Price, Image.

// - Importing Mongoose - \\

const mongoose = require('mongoose'); // * ------------- * \\

// - MongoDB Schema - \

// * ------------- * \\ // - Creating Schema for database - \\

const prodSchema = new mongoose.Schema({

name: String,

description: String,

price: Number,

image: String

}); // - Compiling mongoose Schema to a Model - \\

const Product = mongoose.model('Product', prodSchema); // Exporting Products Model

module.exports = Product;

Right, after we got this done, we will make sure that model is actually being exported so we can use it in other files.

Note: Reason why i am using String for image property is because i am storing links to images. You could setup blobs / binary files or similar if you wish.

STEP 2 - Setting Up Shop Route (shop.js)

Next thing, we will setup a shop route file (shop.js). Shop route will include GET requests for displaying index shop page and product’s page. As well, we will code our small search system.

1. Requiring Dependencies:

// - Setting Up Dependencies - \\

const express = require('express'),

router = express.Router();

// - Importing MVC files (Model-View-Controller design pattern) - \\

const Product = require('../models/Products');

// - Importing Other JS Files - \\

const escapeRegex = require('../../js/utilities/regex-escape');

2. Setting Up GET Request for Shop Index Page:

// GET - Shop Index Page | - Displaying shop index page - \\

router.get('/shop', async (req, res) => {

try {

// Rendering EJS Shop Index File

res.render('shop-index.ejs');

} catch (err) {

throw new Error(err);

}

});

→ Basic GET request route setup, which renders EJS shop index page. It uses async / await. Though, we could use normal callbacks, it won’t make a difference, as all we are doing there is rendering EJS file.

3. Setting Up GET Request for Products Page:

// GET - Shop Product Page | - Displaying demanded product page with page numbers router.get('/shop/:page', async (req, res, next) => {

// Declaring variable

const resPerPage = 9; // results per page

const page = req.params.page || 1; // Page try {

if (req.query.search) {

// Declaring query based/search variables

const searchQuery = req.query.search,

regex = new RegExp(escapeRegex(req.query.search), 'gi'); // Find Demanded Products - Skipping page values, limit results per page

const foundProducts = await Product.find({name:regex})

.skip((resPerPage * page) - resPerPage)

.limit(resPerPage);

// Count how many products were found

const numOfProducts = await Product.count({name: regex});

// Renders The Page

res.render('shop-products.ejs'), {

products: foundProducts,

currentPage: page,

pages: Math.ceil(numOfProducts / resPerPage),

searchVal: searchQuery,

numOfResults: numOfProducts

});

}

} catch (err) {

throw new Error(err);

}

});

→ This one gets a little bit nasty, but it ain’t that complex if you take a good look at it.

- First of all, we setup a GET request route for product page. It takes :page as a parameter, which points out the page we are on.

- Next up, we are declaring some variables. First variable (resPerPage) determines how many products we want to be shown per page. In this case, it’s 9. Second variable (page) determines the page that user is on. By default it’s page 1.

- Next up we opened new try block, and it starts of with an if statement. It says: only if there is a search query, execute the following code.

- We save search query (req.query.search) in a variable named searchQuery. Aside of it, we apply our helper regex-escape function to saved query variable searchQuery. So our helper function extracts what’s inside of the searchQuery variable and we can apply it to next method! req.query.search is coming from the form submission that we will build in our shop-index.ejs file.

- In the next method Product.find({name: regex}), we search for products. That regex variable contains extracted req.search.query data using regex-escape function. Aside of it, we skip the page, so first page actually evaluates to page number 1, using .skip() method. And finally we limit results per page to be 9.

- After that, we count how many products were found using .count() method.

- And finally we render shop-products.ejs page filled in with variables!

4. Exporting Shop Router:

// Exporting Shop Router

module.exports = router;

→ Simply exporting shop.js router.

shop.js file should look like this at the end:

// - Setting Up Dependencies - \\

const express = require('express'),

router = express.Router();

// - Importing MVC files (Model-View-Controller design pattern) - \\

const Product = require('../models/Products');

// - Importing Other JS Files - \\

const escapeRegex = require('../../js/utilities/regex-escape'); // GET - Shop Index Page | - Displaying shop index page - \\

router.get('/shop', async (req, res) => {

try {

// Rendering EJS Shop Index File

res.render('shop-index.ejs');

} catch (err) {

throw new Error(err);

}

});

// GET - Shop Product Page | - Displaying demanded product page with page numbers router.get('/shop/:page', async (req, res, next) => {

// Declaring variable

const resPerPage = 9; // results per page

const page = req.params.page || 1; // Page try {

if (req.query.search) {

// Declaring query based/search variables

const searchQuery = req.query.search,

regex = new RegExp(escapeRegex(req.query.search), 'gi'); // Find Demanded Products - Skipping page values, limit results per page

const foundProducts = await Product.find({name:regex})

.skip((resPerPage * page) - resPerPage)

.limit(resPerPage);

// Count how many products were found

const numOfProducts = await Product.count({name: regex});

// Renders The Page

res.render('shop-products.ejs'), {

products: foundProducts,

currentPage: page,

pages: Math.ceil(numOfProducts / resPerPage),

searchVal: searchQuery,

numOfResults: numOfProducts

});

}

} catch (err) {

throw new Error(err);

}

}); // Exporting Shop Router

module.exports = router;

STEP 3 - BUILDING EJS INDEX SHOP PAGE (shop-index.ejs)

After we got all of the work done on back-end, we can create our first EJS file. We will name it shop-index.ejs.

Skipping head part filled with meta data and all the links, title etc. We will jump right into the main content of it.

<div class="standard-wrapper">

<header>

<div>

<h1>PLACEHOLDER TEXT</h1>

<h2>Search For Products</h2>

</div>

<div class="header-form">

<form action="/shop/1/">

<input type="text" name="search" placeholder="Search">

<button type="submit"></button>

</form>

</div>

</header>

</div>

Note: Make sure that action parameter in form is equal to your GET request in shop.js. In this case as we used router.get(‘/shop/:page’) we must add that number 1 to action param (action=’/shop/1’) in the form, so it starts from page 1.

STEP 4 - BUILDING EJS SHOP PRODUCTS PAGE (shop-products.ejs)

Here comes the real part. We will build our actual product page. We will name this file shop-products.ejs.

We will skip head part once again and get right onto the main part of it.

1. Building Results Container / Part of the page| Displays products data (number of products, number of pages, search query)

<!-- SEARCH RESULTS | START -->

<div class="results">

<h3>Search results: <span><%= searchVal %></span></h3>

<h4>Displaying Total <span><%= numOfResults %></span> Results</h4>

<h4>

Page

<span><%= currentPage %></span> // 1

of

<span><%= pages %></span> // 2

</h4> // Displays: Page 1 of 2, depends on how many pages there are

</div>

<!-- SEARCH RESULTS | END -->

→ It’s a simple container / part of the page we built that displays what user searched for, how many results are shown to user, how many pages exists for searched product.

2. Rendering Products

<!-- SHOP | START -->

<section class="shop">

<div class="shop-container"> <!-- Rendering Products using EJS -->

<% products.forEach(product => { %>

<div class="item">

<div class="shop-row shop-row--1">

<img data-lazy="<%= product.image %>" class="cover-img">

</div>

<div class="item-desc">

<h2 class="item-desc__name"><%= product.name %></h2>

<p class="item-desc__text"><%= product.description %></p>

<h3 class="item-desc__price">$ <%= product.price %>

</div>

</div>

<% }); %>

</div>

→ We render products using EJS and forEach loop. For each product in our database, we render new div filled with other divs filled with specific data, such as name, description, price, image.

3. Building Pagination

<!-- PAGINATION -->

<div class="shop-pagination">

<div class="pagination">

<% if (currentPage == 1 && pages > 1) { %>

<a

href="/shop/<%= parseInt(currentPage) + 1 %>/?search=<%=searchVal%>" <span>

Page <%= parseInt(currentPage) + 1 %>

<i class="icon ion-ios-arrow-forward"></i>

</span>

</a> <% } else if (currentPage < pages) { %>

<a

href="/shop/<%= parseInt(currentPage) - 1 %>/?search<%=searchVal%>"><span>

<i class="icon ion-ios-arrow-back"></i>

Page <%= parseInt(currentPage) - 1 %>

</span>

</a> <a

href="/shop/<%= parseInt(currentPage) + 1 %>/?search<%=searchVal%>"><span>

Page <%= parseInt(currentPage) + 1 %>

<i class="icon ion-ios-arrow-forward"></i>

</span>

</a> <% } else if (currentPage == pages && pages > 1) { %>

<a

href="/shop/<%= parseInt(currentPage) - 1 %>/?search<%=searchVal%>"><span>

<i class="icon ion-ios-arrow-back"></i>Page

<%=parseInt(currentPage) - 1 %>

</span>

</a> <% } %>

</div>

→ Here we build actual pagination. First of all, if currentPage is 1, only render button to go forward. Else if currentPage is less than actual amount of pages (which means we are somewhere in the middle, ex: page 5 out of 9), render both buttons, forward and backward. And finally at the end, if currentPage is equal to total amount of pages and there is more than 1 page (we are on the last page), only render the button to go backward.

If results are less than 9 or 9, pagination won’t render.

Final shop-products.ejs file should look like this: