Node.js AJAX Pagination with MongoDB + Search + Sort + Filter

Date Posted: September 12, 2016, 6:12 pm


For this tutorial we will be using Jade Templating, you can use other templating modules but I’m afraid I wont be able to provide you further support. Jade templating is easy to learn, if you know HTML then it should only take you a few minutes to learn Jade. One rule you have to take note when using Jade is to be very careful with the spaces, this tutorial make use of tabs for indention. Another thing to note when using Jade is you can only choose one kind of indention – spaces or tabs, but you can’t use both at the same time.

This tutorial make use of Express Module so you should have that setup before getting started. I would assume that you already have basic knowledge working with MongoDB and that you already have it set up in your computer.

nodejs-mongodb-ajax-pagination-search-sort-filter

Required Modules

Let’s Start Coding

We first create all the fields for our search, sort and filters as well as the container that we will be using for rendering the pagination. You can save the code bellow in a file named products.jade under your views folder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
extends layout

block content
    .container.products-view-all
        form.post-list
            input(type='hidden', value='')
        .clearfix
            article.navbar-form.navbar-left.p-0.m-0.ml-b
                .form-group
                    label Per Page:
                    select.form-control.post_max.m-b
                        option(value='20') 20
                        option(value='50') 50
                        option(value='100') 100
                label Search Keyword:
                    input.form-control.post_search_text.m-b(type='text', placeholder='Enter a keyword')
                .form-group
                    label Order By:
                    select.form-control.post_name.m-b
                        option(value='name') Title
                        option(value='price') Price
                        option(value='quantity') Quantity
                    select.form-control.post_sort.m-b
                        option(value='ASC') ASC
                        option(value='DESC') DESC
                input.btn.btn-primary.post_search_submit.m-b(type='submit', value='Filter')
        hr
           
        .clearfix
            .pagination-container.clearfix
            .pagination-nav

Include MongoDB Connection in your app.js

1
2
3
4
/* MongoDB connection */
var mongo = require('mongodb');
var monk = require('monk');
var db = monk('localhost:27017/nodetest1'); // 27017 is the default port for our MongoDB instance.

To make our DB accessible to our router, add the following lines in your app.js (just above app.use(‘/’, routes);)

1
2
3
4
app.use(function(req, res, next){
    req.db = db;
    next();
});

Client Side

The script bellow is the handler of all user clicks coming from the front-end. You can save it in a file called MyPaginationClass.js under public/javascripts. Make sure to include this script file in your layout.jade.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/**
 * App Class
 *
 * @author      Carl Victor Fontanos
 * @author_url  www.carlofontanos.com
 *
 */


var app = {
   
    Posts: function() {
       
        /**
         * This method contains the list of functions that needs to be loaded
         * when the "Posts" object is instantiated.
         *
         */

        this.init = function() {
            this.get_all_items_pagination();
        }
       
        /**
         * Load front-end items pagination.
         */

        this.get_all_items_pagination = function() {
           
            _this = this;
           
            /* Check if our hidden form input is not empty, meaning it's not the first time viewing the page. */
            if($('form.post-list input').val()){
                /* Submit hidden form input value to load previous page number */
                data = JSON.parse($('form.post-list input').val());
                _this.ajax_get_all_items_pagination(data.page, data.name, data.sort);
            } else {
                /* Load first page */
                _this.ajax_get_all_items_pagination(1, $('.post_name').val(), $('.post_sort').val());
            }
                   
            /* Search */
            $('body').on('click', '.post_search_submit', function(){
                _this.ajax_get_all_items_pagination(1, $('.post_name').val(), $('.post_sort').val());
            });
            /* Search when Enter Key is triggered */
            $(".post_search_text").keyup(function (e) {
                if (e.keyCode == 13) {
                    _this.ajax_get_all_items_pagination(1, $('.post_name').val(), $('.post_sort').val());
                }
            });
           
            /* Pagination Clicks   */                  
            $('body').on('click', '.pagination-nav li.active', function(){
                var page = $(this).attr('p');
                _this.ajax_get_all_items_pagination(page, $('.post_name').val(), $('.post_sort').val());                
            });
        }
       
        /**
         * AJAX front-end items pagination.
         */

        this.ajax_get_all_items_pagination = function(page, order_by_name, order_by_sort){
           
            if($(".pagination-container").length > 0 && $('.products-view-all').length > 0 ){
                $(".pagination-container").html('<img src="images/loading.gif" class="ml-tb" />');
               
                var post_data = {
                    page: page,
                    search: $('.post_search_text').val(),
                    name: order_by_name,
                    sort: order_by_sort,
                    max: $('.post_max').val(),
                };
               
                $('form.post-list input').val(JSON.stringify(post_data));
               
                var data = {
                    action: 'get-all-products',
                    data: JSON.parse($('form.post-list input').val())
                };
               
                $.ajax({
                    url: 'products/view-front',
                    type: 'POST',
                    contentType: 'application/json',
                    data: JSON.stringify(data),
                    success: function (response) {
                       
                        if($(".pagination-container").html(response.content)){
                            $('.pagination-nav').html(response.navigation);
                            $('.table-post-list th').each(function() {
                                /* Append the button indicator */
                                $(this).find('span.glyphicon').remove();    
                                if($(this).hasClass('active')){
                                    if(JSON.parse($('form.post-list input').val()).th_sort == 'DESC'){
                                        $(this).append(' <span class="glyphicon glyphicon-chevron-down pull-right"></span>');
                                    } else {
                                        $(this).append(' <span class="glyphicon glyphicon-chevron-up pull-right"></span>');
                                    }
                                }
                            });
                        }
                    }
                });
            }
        }
    }
}

/**
 * When the document has been loaded...
 *
 */

jQuery(document).ready( function () {
    posts = new app.Posts(); /* Instantiate the Posts Class */
    posts.init(); /* Load Posts class methods */   
});

Routes

Routing refers to determining how an application responds to a client request to a particular endpoint, which is a URI (or path) and a specific HTTP request method (GET, POST, and so on). When you install the Express module, it includes 2 routes: index.js and users.js. Open the file index.js and include the codes bellow:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
var express = require('express');
var router = express.Router();
var async = require('async');

/* GET products page. */
router.get('/products', function(req, res, next) {
    res.render('front/products', { title: 'Products' });
});

/* Handles the pagination. */
router.post('/products/view-front', function(req, res){
   
    /* Set our internal DB variable */
    var db = req.db;
   
        /* Set our collection */
        products = db.get('products');
       
        pag_content = '';
        pag_navigation = '';
   
        page = parseInt(req.body.data.page); /* Page we are currently at */
        name = req.body.data.name; /* Name of the column name we want to sort */
        sort = req.body.data.sort == 'ASC' ? 1 : -1; /* Order of our sort (DESC or ASC) */
        max = parseInt(req.body.data.max); /* Number of items to display per page */
        search = req.body.data.search; /* Keyword provided on our search box */
       
        cur_page = page;
        page -= 1;
        per_page = max ? max : 20;
        previous_btn = true;
        next_btn = true;
        first_btn = true;
        last_btn = true;
        start = page * per_page;
   
        where_search = {};
   
    /* Check if there is a string inputted on the search box */
    if( search != '' ){
        /* If a string is inputted, include an additional query logic to our main query to filter the results */
        var filter = new RegExp(search, 'i');
        where_search = {
            '$or' : [
                {'name' : filter},
                {'price' : filter},
            ]
        }
    }
   
    var all_items = '';
    var count = '';
    var sort_query = {};
   
    /* We use async task to make sure we only return data when all queries completed successfully */
    async.parallel([
        function(callback) {
            /* Use name and sort variables as field names */
            sort_query[name] = sort;
           
            /* Retrieve all the posts */
            products.find( where_search, {
                limit: per_page,
                skip: start,
                sort: sort_query
               
            }, function(err, docs){
                if (err) throw err;
                // console.log(docs);
                all_items = docs;
                callback();
               
            });
        },
        function(callback) {
            products.count(where_search, function(err, doc_count){
                if (err) throw err;
                // console.log(count);
                count = doc_count;
                callback();
            });
        }
    ], function(err) { //This is the final callback
        /* Check if our query returns anything. */
        if( count ){
            for (var key in all_items) {
                pag_content += '<div class="col-sm-3">' +
                    '<div class="panel panel-default">' +
                        '<div class="panel-heading">' +
                            all_items[key].name +
                        '</div>' +
                        '<div class="panel-body p-0 p-b">' +
                            '<a href="products-single.php?item=' + all_items[key]._id + '"><img src="img/uploads/' + all_items[key].featured_image + '" width="100%" class="img-responsive" /></a>' +
                            '<div class="list-group m-0">' +
                                '<div class="list-group-item b-0 b-t">' +
                                    '<i class="fa fa-calendar-o fa-2x pull-left ml-r"></i>' +
                                    '<p class="list-group-item-text">Price</p>' +
                                    '<h4 class="list-group-item-heading">$' + parseFloat(all_items[key].price).toFixed(2) + '</h4>' +
                                '</div>' +
                                '<div class="list-group-item b-0 b-t">' +
                                    '<i class="fa fa-calendar fa-2x pull-left ml-r"></i>' +
                                    '<p class="list-group-item-text">Quantity</p>' +
                                    '<h4 class="list-group-item-heading">' + all_items[key].quantity + '</h4>' +
                                '</div>' +
                            '</div>' +
                        '</div>' +
                        '<div class="panel-footer">' +
                            '</p><a href="products-single.php?item=' + all_items[key]._id + '" class="btn btn-success btn-block">View Item</a></p>' +
                         '</div>' +
                    '</div>' +
                '</div>';
            }
        }
       
        pag_content = pag_content + "<br class = 'clear' />";
       
        no_of_paginations = Math.ceil(count / per_page);

        if (cur_page >= 7) {
            start_loop = cur_page - 3;
            if (no_of_paginations > cur_page + 3)
                end_loop = cur_page + 3;
            else if (cur_page <= no_of_paginations && cur_page > no_of_paginations - 6) {
                start_loop = no_of_paginations - 6;
                end_loop = no_of_paginations;
            } else {
                end_loop = no_of_paginations;
            }
        } else {
            start_loop = 1;
            if (no_of_paginations > 7)
                end_loop = 7;
            else
                end_loop = no_of_paginations;
        }
         
        pag_navigation += "<ul>";

        if (first_btn && cur_page > 1) {
            pag_navigation += "<li p='1' class='active'>First</li>";
        } else if (first_btn) {
            pag_navigation += "<li p='1' class='inactive'>First</li>";
        }

        if (previous_btn && cur_page > 1) {
            pre = cur_page - 1;
            pag_navigation += "<li p='" + pre + "' class='active'>Previous</li>";
        } else if (previous_btn) {
            pag_navigation += "<li class='inactive'>Previous</li>";
        }
        for (i = start_loop; i <= end_loop; i++) {

            if (cur_page == i)
                pag_navigation += "<li p='" + i + "' class = 'selected' >" + i + "</li>";
            else
                pag_navigation += "<li p='" + i + "' class='active'>" + i + "</li>";
        }
       
        if (next_btn && cur_page < no_of_paginations) {
            nex = cur_page + 1;
            pag_navigation += "<li p='" + nex + "' class='active'>Next</li>";
        } else if (next_btn) {
            pag_navigation += "<li class='inactive'>Next</li>";
        }

        if (last_btn && cur_page < no_of_paginations) {
            pag_navigation += "<li p='" + no_of_paginations + "' class='active'>Last</li>";
        } else if (last_btn) {
            pag_navigation += "<li p='" + no_of_paginations + "' class='inactive'>Last</li>";
        }

        pag_navigation = pag_navigation + "</ul>";
       
        var response = {
            'content': pag_content,
            'navigation' : pag_navigation
        };
       
        res.send(response);
       
    });

});

module.exports = router;

What the code above does is it tells our server what to render(GET) and what to do(POST) when the page is accessed. We have defined two routes:

  • /products – When the user types in /products on their URL bar, we serve them our products.jade template view.
  • /products/view-front – This route processes all user clicks coming from products page. When it receives a post request from our MyPaginationClass.js via AJAX, it will query for data that matches the query string(s) of our user input and then render it all back to our browser screen via the res.send() function. If you notice we are using Async Task – this is necessary so we can tell NodeJS to wait until all our MongoDB queries finished executing before processing the query results in our pagination. This may seem like we created a blocking script but the Async Module already helps prevent that kind of case.

Style the Nagivation

Add this to your stylesheet:

1
2
3
4
5
6
7
/* Pagination Styles */
.pagination-nav { margin: 30px 0 20px 0; }
.pagination-nav ul {margin: 0; padding: 0;}
.pagination-nav ul li {display: inline-block; margin: 3px; padding: 4px 8px; background: #FFF; color: black; }
.pagination-nav ul li.active:hover {cursor: pointer; background: #367fa9; color: white; }
.pagination-nav ul li.inactive {background: #CACACA;}
.pagination-nav ul li.selected {background: #367fa9; color: white;}

That’s it, we’re done!

You can now test the code by navigating to http://27.0.0.1:3000/products

About author


Carl Victor Fontanos

A fine gentleman specializing in front-end and back-end development with extensive experience building high performance web applications that keeps users engaged and help businesses grow.

 
Hire Me