2
\$\begingroup\$

I have made a small application that displays a posts JSON in the form of cards, with the help of AngularJS and Twitter Bootstrap 4.

The application has an interface for pagination and there are about 100 posts displayed on each page.

var root = 'https://jsonplaceholder.typicode.com';

// Create an Angular module named "postsApp"
var app = angular.module("postsApp", []);

// Create controller for the "postsApp" module
app.controller("postsCtrl", ["$scope", "$http", "$filter", function($scope, $http, $filter) {
  var url = root + "/posts";
  $scope.postList = [];
  $scope.search = "";
  $scope.filterList = function() {
    var oldList = $scope.postList || [];
    $scope.postList = $filter('filter')($scope.posts, $scope.search);
    if (oldList.length != $scope.postList.length) {
      $scope.pageNum = 1;
      $scope.startAt = 0;
    };
    $scope.itemsCount = $scope.postList.length;
    $scope.pageMax = Math.ceil($scope.itemsCount / $scope.perPage);
  };
  $http.get(url)
    .then(function(data) {
      // posts arary
      $scope.posts = data.data;
      $scope.filterList();

      // Paginate
      $scope.pageNum = 1;
      $scope.perPage = 24;
      $scope.startAt = 0;
      $scope.filterList();

      $scope.currentPage = function(index) {
        $scope.pageNum = index + 1;
        $scope.startAt = index * $scope.perPage;
      };

      $scope.prevPage = function() {
        if ($scope.pageNum > 1) {
          $scope.pageNum = $scope.pageNum - 1;
          $scope.startAt = ($scope.pageNum - 1) * $scope.perPage;
        }
      };

      $scope.nextPage = function() {
        if ($scope.pageNum < $scope.pageMax) {
          $scope.pageNum = $scope.pageNum + 1;
          $scope.startAt = ($scope.pageNum - 1) * $scope.perPage;
        }
      };
    });
}]);
.posts-grid {
  margin-top: 25px;
  display: flex;
  flex-wrap: wrap;
}

.posts-grid>[class*='col-'] {
  display: flex;
  flex-direction: column;
  margin-bottom: 25px;
}

.posts-grid .post {
  background: #fff;
  border-top: 1px solid #d5d5d5;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.11);
}

.posts-grid .text {
  padding: 8px;
}

.posts-grid .card-title {
  font-size: 1.25rem;
  margin-bottom: 8px;
  text-transform: capitalize;
}

.posts-grid .read-more {
  padding: 0 8px 8px 8px;
}

.posts-grid .text-muted {
  margin-bottom: 8px;
}

.posts-grid .thumbnail img {
  display: block;
  width: 100%;
  height: auto;
}

.posts-grid p {
  text-align: justify;
}

.posts-grid .post {
  flex-grow: 1;
  display: flex;
  flex-direction: column;
}

.posts-grid .read-more {
  margin-top: auto;
}

.pagination>li>a,
.pagination>li>a:hover,
.pagination>li>span {
  color: #585858;
  line-height: 1;
  padding: 6px 12px;
  text-decoration: none;
}

.pagination>.active>a,
.pagination>.active>span,
.pagination>.active>a:hover,
.pagination>.active>span:hover,
.pagination>.active>a:focus,
.pagination>.active>span:focus {
  background-color: #007bff;
  border-color: #2b7c2b;
  color: #fff;
}

@media (max-width: 767px) {
  .container {
    max-width: 100%;
  }
}

@media (max-width: 575px) {
  .container {
    max-width: 100%;
    padding-left: 0;
    padding-right: 0;
  }
  .posts-grid>[class*='col-'] {
    padding-left: 5px;
    padding-right: 5px;
  }
}
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular.js"></script>

<nav class="navbar navbar-expand-md bg-dark navbar-dark sticky-top">
  <!-- Brand -->
  <a class="navbar-brand" href="#">My Blog</a>
  <!-- Toggler/collapsibe Button -->
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#collapsibleNavbar">
			<span class="navbar-toggler-icon"></span>
  </button>

  <!-- Navbar links -->
  <div class="collapse navbar-collapse" id="collapsibleNavbar">
    <ul class="navbar-nav ml-auto">
      <li class="nav-item">
        <a class="nav-link active" href="#">Contacts</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="#">About us</a>
      </li>
      <li class="nav-item">
        <a class="nav-link btn btn-outline-primary" href="#">Login</a>
      </li>
    </ul>
  </div>
</nav>

<div data-ng-app="postsApp">
  <div class="container" data-ng-controller="postsCtrl">
    <div class="row">
      <div class="col-sm-9 mx-auto">
        <div class="form-group search-box mt-3 px-3">
          <input type="text" class="form-control" id="search" placeholder="Search post" data-ng-model="search" ng-change="filterList()">
        </div>
      </div>
    </div>
    <div class="posts-grid" ng-if="postList.length > 0">
      <div class="col-xs-12 col-sm-6 col-lg-4 col-xl-3" data-ng-repeat="post in postList | limitTo : perPage : startAt">
        <div class="post">
          <div class="thumbnail">
            <img src="//lorempixel.com/450/300" />
          </div>
          <div class="text">
            <h3 class="card-title">{{post.title}}</h3>
            <p class="text-muted">{{post.body}}</p>
          </div>
          <div class="read-more">
            <a class="btn btn-block btn-sm btn-primary" href="#">Read more</a>
          </div>
        </div>
      </div>
    </div>
    <p ng-if="postList.length <= 0" class="text-center">There are no posts</p>
    <div ng-if="pageMax > 1">
      <ul class="pagination pagination-sm justify-content-center">
        <li class="page-item"><a href="#" ng-click="prevPage()"><i class="fa fa-chevron-left"></i></a></li>
        <li ng-repeat="n in [].constructor(pageMax) track by $index" ng-class="{true: 'active'}[$index == pageNum - 1]">
          <a href="#" ng-click="currentPage($index)">{{$index+1}}</a>
        </li>
        <li><a href="#" ng-click="nextPage()"><i class="fa fa-chevron-right"></i></a></li>
      </ul>
    </div>
  </div>
</div>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js"></script>
<script>
  $(document).ready(function($) {
    $('.pagination > li > a').click(function() {
      $("html, body").animate({
        scrollTop: 0
      }, 500);
      return false;
    });
  });
</script>

A few questions ware born in my mind recently and I did not find the answer, hence my topic here:

  1. What if there ware 10 or 100 times as many posts, would the application have a performance problem (e.g. with page load)?
  2. Is there a better way to paginate the application; one that would load as many items from posts.json as there are displayed on one page (24 items), instead of the entire JSON file?
  3. How would you optimize this application, on the front end?
\$\endgroup\$
  • \$\begingroup\$ Note: this post appears to be cross-posted on SO and also has an accepted answer there \$\endgroup\$ – Sᴀᴍ Onᴇᴌᴀ Dec 10 '18 at 21:31
  • \$\begingroup\$ @SᴀᴍOnᴇᴌᴀ I think the 2 websites have different purposes. Am I wrong? \$\endgroup\$ – Razvan Zamfir Dec 10 '18 at 21:39
  • \$\begingroup\$ No you are not wrong - they have different purposes; I just wanted to add context for anybody else viewing this post \$\endgroup\$ – Sᴀᴍ Onᴇᴌᴀ Dec 11 '18 at 17:50
1
\$\begingroup\$
  1. What if there ware 10 or 100 times as many posts, would the application have a performance (page load) problem?

Try it with 10 times as many posts. I know that just duplicates each post 10 times without modifying the unique values like id but it should demonstrate the performance. I can see the page links but unless I make the window full screen and zoom out (on my 24" monitor) I can't see the all the page links - e.g. the link for Page 1 is cut off, as well as anything beyond page 42.

  1. Is there a better way to paginate the application; one that would load as many items from posts.json as there are displayed on one page (24 items), instead of the entire JSON file?

Unless the API endpoint accepts a parameter for the page size, you would likely need to utilize a server-side scripting language like Python, Ruby on Rails, PHP, C#, VB.NET, ASP, etc. which could return the specified pagesize.

  1. How would you optimize this application, on the front end?

I don't see much that stands out as an obvious place to optimize, though I do notice that the promise callback to $http.get(url) has two calls to filterList() separated by three assignment lines:

$http.get(url)
    .then(function(data) {
      // posts arary
      $scope.posts = data.data;
      $scope.filterList();

      // Paginate
      $scope.pageNum = 1;
      $scope.perPage = 24;
      $scope.startAt = 0;
      $scope.filterList();

Is there any way to reduce the two calls to a single call there? Perhaps the first call is not needed?

Then looking at that promise callback, I see the three methods set on $scope there: currentPage, prevPage and nextPage. I would recommend moving those methods out of the callback handler, so that all it does is set the post data on $scope and related paging variables.

Also, I see a flaw with the jQuery code used for scrolling to the top (which doesn't appear to be working because there are no elements matching the selector .pagination > li > a when the DOM is loaded). I tried finding an equivalent in AngularJS but found little... There is a solution I found from this post that uses vanillaJS. I had to alter the href attributes on the anchors to use it.

$scope.scrollToTop = function() {
    var scrollDuration = 500;
    var scrollStep = -window.scrollY / (scrollDuration / 10);

    var scrollInterval = setInterval(function() {
      if (window.scrollY != 0) {
        window.scrollBy(0, scrollStep);
      } else {
        clearInterval(scrollInterval);
      }
    }, 15);
  };

Then that can be used in the click handler methods:

  $scope.currentPage = function(index) {
    $scope.pageNum = index + 1;
    $scope.startAt = index * $scope.perPage;
    $scope.scrollToTop();
  };

Though in order to stop the anchor navigation, the default event handling will need to be prevented:

<a href="#" ng-click="currentPage($index); $event.preventDefault();">{{$index+1}}</a>

Updated code

See modified code below.

var root = 'https://jsonplaceholder.typicode.com';

// Create an Angular module named "postsApp"
var app = angular.module("postsApp", []);

// Create controller for the "postsApp" module
app.controller("postsCtrl", ["$scope", "$http", "$filter", function($scope, $http, $filter) {
  var url = root + "/posts";
  $scope.postList = [];
  $scope.search = "";
  $scope.filterList = function() {
    var oldList = $scope.postList || [];
    $scope.postList = $filter('filter')($scope.posts, $scope.search);
    if (oldList.length != $scope.postList.length) {
      $scope.pageNum = 1;
      $scope.startAt = 0;
    };
    $scope.itemsCount = $scope.postList.length;
    $scope.pageMax = Math.ceil($scope.itemsCount / $scope.perPage);
  };


  $scope.currentPage = function(index) {
    $scope.pageNum = index + 1;
    $scope.startAt = index * $scope.perPage;
    $scope.scrollToTop();
  };

  $scope.prevPage = function() {
    if ($scope.pageNum > 1) {
      $scope.pageNum = $scope.pageNum - 1;
      $scope.startAt = ($scope.pageNum - 1) * $scope.perPage;
      $scope.scrollToTop();
    }
  };

  $scope.nextPage = function() {
    if ($scope.pageNum < $scope.pageMax) {
      $scope.pageNum = $scope.pageNum + 1;
      $scope.startAt = ($scope.pageNum - 1) * $scope.perPage;
      $scope.scrollToTop();
    }
  };
  $scope.scrollToTop = function() {
    var scrollDuration = 500;
    var scrollStep = -window.scrollY / (scrollDuration / 10);

    var scrollInterval = setInterval(function() {
      if (window.scrollY != 0) {
        window.scrollBy(0, scrollStep);
      } else {
        clearInterval(scrollInterval);
      }
    }, 15);
  };
  $http.get(url)
    .then(function(data) {
      // posts arary
      $scope.posts = data.data;
      $scope.filterList();

      // Paginate
      $scope.pageNum = 1;
      $scope.perPage = 24;
      $scope.startAt = 0;
      $scope.filterList();
    });
}]);
.posts-grid {
  margin-top: 25px;
  display: flex;
  flex-wrap: wrap;
}

.posts-grid>[class*='col-'] {
  display: flex;
  flex-direction: column;
  margin-bottom: 25px;
}

.posts-grid .post {
  background: #fff;
  border-top: 1px solid #d5d5d5;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.11);
}

.posts-grid .text {
  padding: 8px;
}

.posts-grid .card-title {
  font-size: 1.25rem;
  margin-bottom: 8px;
  text-transform: capitalize;
}

.posts-grid .read-more {
  padding: 0 8px 8px 8px;
}

.posts-grid .text-muted {
  margin-bottom: 8px;
}

.posts-grid .thumbnail img {
  display: block;
  width: 100%;
  height: auto;
}

.posts-grid p {
  text-align: justify;
}

.posts-grid .post {
  flex-grow: 1;
  display: flex;
  flex-direction: column;
}

.posts-grid .read-more {
  margin-top: auto;
}

.pagination>li>a,
.pagination>li>a:hover,
.pagination>li>span {
  color: #585858;
  line-height: 1;
  padding: 6px 12px;
  text-decoration: none;
}

.pagination>.active>a,
.pagination>.active>span,
.pagination>.active>a:hover,
.pagination>.active>span:hover,
.pagination>.active>a:focus,
.pagination>.active>span:focus {
  background-color: #007bff;
  border-color: #2b7c2b;
  color: #fff;
}

@media (max-width: 767px) {
  .container {
    max-width: 100%;
  }
}

@media (max-width: 575px) {
  .container {
    max-width: 100%;
    padding-left: 0;
    padding-right: 0;
  }
  .posts-grid>[class*='col-'] {
    padding-left: 5px;
    padding-right: 5px;
  }
}
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js"></script>


<nav class="navbar navbar-expand-md bg-dark navbar-dark sticky-top">
  <!-- Brand -->
  <a class="navbar-brand" href="#">My Blog</a>
  <!-- Toggler/collapsibe Button -->
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#collapsibleNavbar">
			<span class="navbar-toggler-icon"></span>
  </button>

  <!-- Navbar links -->
  <div class="collapse navbar-collapse" id="collapsibleNavbar">
    <ul class="navbar-nav ml-auto">
      <li class="nav-item">
        <a class="nav-link active" href="#">Contacts</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="#">About us</a>
      </li>
      <li class="nav-item">
        <a class="nav-link btn btn-outline-primary" href="#">Login</a>
      </li>
    </ul>
  </div>
</nav>

<div data-ng-app="postsApp">
  <div class="container" data-ng-controller="postsCtrl">
    <div class="row">
      <div class="col-sm-9 mx-auto">
        <div class="form-group search-box mt-3 px-3">
          <input type="text" class="form-control" id="search" placeholder="Search post" data-ng-model="search" ng-change="filterList()">
        </div>
      </div>
    </div>
    <div class="posts-grid" ng-if="postList.length > 0">
      <div class="col-xs-12 col-sm-6 col-lg-4 col-xl-3" data-ng-repeat="post in postList | limitTo : perPage : startAt">
        <div class="post">
          <div class="thumbnail">
            <img src="//lorempixel.com/450/300" />
          </div>
          <div class="text">
            <h3 class="card-title">{{post.title}}</h3>
            <p class="text-muted">{{post.body}}</p>
          </div>
          <div class="read-more">
            <a class="btn btn-block btn-sm btn-primary" href="#">Read more</a>
          </div>
        </div>
      </div>
    </div>
    <p ng-if="postList.length <= 0" class="text-center">There are no posts</p>
    <div ng-if="pageMax > 1">
      <ul class="pagination pagination-sm justify-content-center">
        <li class="page-item"><a href="#" ng-click="prevPage(); $event.preventDefault();"><i class="fa fa-chevron-left"></i></a></li>
        <li ng-repeat="n in [].constructor(pageMax) track by $index" ng-class="{true: 'active'}[$index == pageNum - 1]">
          <a href="#" ng-click="currentPage($index); $event.preventDefault();">{{$index+1}}</a>
        </li>
        <li><a href="#" ng-click="nextPage(); $event.preventDefault();"><i class="fa fa-chevron-right"></i></a></li>
      </ul>
    </div>
  </div>
</div>

\$\endgroup\$
  • \$\begingroup\$ Please try an answer this question on Stackoverflow. Thanks! \$\endgroup\$ – Razvan Zamfir Dec 10 '18 at 21:05
  • \$\begingroup\$ okay; Was this answer useful/helpful? \$\endgroup\$ – Sᴀᴍ Onᴇᴌᴀ Dec 10 '18 at 21:21
  • \$\begingroup\$ Yes. I need to improve the project further. By turning the pagination into a service. \$\endgroup\$ – Razvan Zamfir Dec 10 '18 at 21:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.