Series Hướng dẫn xây dựng ứng dụng bán hàng bằng angular js kết hợp với Rails(Phần 4)

Lan man, luyên thuyên

Hey các bạn! Lại một tháng nữa trôi qua, tháng vừa qua thì Đà Nẵng trở mình trong cái ẩm ướt và một chút không khí lạnh. Nó cũng khiến con người ta dễ lười đi. hì hì ^^ Hôm nay tôi sẽ cùng với các bạn tiếp tục những Series xây dựng ứng dụng bán hàng bằng angular js kết hợp với Rails(Phần 4) Những phần cần đánh hôm nay đó chính là :

  • Phân loại sản phẩm theo các categories
  • Phân trang cho sản phẩm. Chỉ từng đấy thôi, tôi nghĩ là chúng ta lướt qua khá nhanh đấy 😄

1. ## Phân loại sản phẩm theo các categories

Để sản phẩm có thể load theo 1 category khi chúng ta chọn nó thì phần controller tôi sẽ load như sau:

def category
    Category.find_by(id: params[:category_id])
end

ở action Index nó sẽ load đè biến json đã được filter từ trước:

def index
    @newest_items = Item.newest
    newest_items = Item.in_range(params[:price_range]).newest
    newest_items = category.items.newest if category
    return unless request.format.json?
    render json: {
      newest_items: newest_items
    }
  end

Ở ngay dòng đầu tiên có thể bạn sẽ thắc mắc nó hơi khác với trước, lý do là tôi muốn lấy tất cả ra rồi phân trang ra cho các bạn thấy Phần code ở view và controller angular cũng có chút thay đổi

<div class="panel panel-default" ng-repeat="category in vm.categories">
    <div class="panel-heading">
      <h4 class="panel-title">
        <a data-toggle="collapse" data-parent="#accordian" href="#{{category.name}}">
          <span class="badge pull-right" ng-show="category.child.length != 0"><i class="fa fa-plus"></i></span>
          {{category.name}}
        </a>
      </h4>
    </div>
    <div id="{{category.name}}" class="panel-collapse collapse">
      <div class="panel-body">
        <ul ng-repeat="child in category.child">
          <li><a href="javascript:void(0)" ng-click="vm.filterByCategory(child.id)">{{child.name}}</a></li>
        </ul>
      </div>
    </div>
  </div>

Ta đi định nghĩa một action để lấy ra CategoryID của nó

vm.filterByCategory = function(categoryID){
    filterItemService.filterByCategory(categoryID).then(function(res) {
      vm.itemNewest = res.newest_items;
    }, function(err){
        // ở đây bạn làm gì tuỳ thích nếu như được trả về lỗi, có thể bắn ra flash hay chuyển trang vv...
    });
  };

Cũng tương tự như lần trước mình filter sản phẩm theo giá, thì lần này cũng như vậy, mình cần 1 function được định nghĩa trong services nhằm đẩy params lên server và lấy kết quả về

function filterByCategory(categoryID) {
    var url = '/' + '?category_id=' + categoryID;
    return common.ajaxCall('GET', url, categoryID);
}

và đừng quên khai báo trả về:

return {
    filterByPrice: filterByPrice,
    filterByCategory: filterByCategory
  };

Ở đây bạn có thể lồng vào 1 hàm trong service, đồng thời kẹp vào điều kiện để kiểm tra nhưng mình không khuyến khích cho những bạn mới. ^^ Mình cứ từ từ mà làm, k gì phải vội refactor code trước đúng không ạ 😛 Như vậy là đã xong phần phân loại sản phẩm theo các categories. Bạn có thể thử nhé. Còn bây giờ chúng ta sẽ tiếp tục đi qua phần nữa của series ngày hôm nay

2. ## Phân Trang sản phẩm bằng Angularjs

Mình xin được nói về ý tưởng phân trang một chút, ở đây mình sẽ phân trang sản phẩm hoàn toàn bằng angularjs, sẽ không đẩy params hay nhờ server phân trang, nghĩa là mình sẽ phân trang ngay và luôn tại client. Để làm được điều này bạn cần phân tích những thứ cần và những thứ đủ cho công việc này. Mình sẽ có những input như sau:

  • Tổng sổ sản phẩm mà chúng ta có (totalItems)
  • Tổng số trang mà chúng ta sẽ chia ra (totalPages)
  • Tổng số sản phẩm hiển thị trên 1 trang (pageSize) thằng này => chúng ta sẽ tự config
  • Tổng số trang mà chúng ta sẽ hiển thị ra view (displayPages) thằng này => chúng ta cũng sẽ tự config
  • Một mảng trang tịnh tiến để hiển thị ra view (pages)
  • Số trang mà chúng ta đang thao tác (currentPage) Tính toán một chút chúng ta sẽ thấy:
  1. Tổng số trang cần chia = Tổng sổ sản phẩm mà chúng ta có/ Tổng số sản phẩm hiển thị trên 1 trang. Và nhớ đem con số này làm tròn nhé
    totalPages = Math.ceil(totalItems / pageSize);

Có nghĩa là ở đây chúng ta đã tính toán ra được với số sản phẩm đó chúng ta sẽ có 1 mảng là totalPages trang Về lý thuyết là vậy nhưng chúng ta vẫn cần phải xử lý con số này thêm 1 lần nữa, để nó thành 1 khoảng để có thể hiển thị hợp lý, vì chúng ta không thể hiện thị tất cả số trang mà chúng ta đã tính toán ở trên. Ví dụ 3000Tổng sản phẩm mà chúng ta có)/6(Tổng số sản phẩm hiển thị mỗi trang) = 500 hưng chúng ta không thể hiển thị 50 trang này ra list button phân trang được đúng không nào. Vậy để xử lý chúng ta làm như sau:

      var pages = _.range(startPage, endPage);

Để có được 1 mảng pages này hợp lý thì thằng startPage và thằng endPage sẽ được tính toán tiếp tục như sau:

    // gán mặc định cho pages đang thao tác, nếu không có thì là sẽ là trang 1
      currentPage = currentPage || 1;

      // gán mặc định cho số sản phẩm hiển thị trên trang. Ban đầu nó sẽ là số mình gán, nhưng về trang cuối cùng thì số này sẽ là 1 số <= 6
      pageSize = pageSize || 6;
      var displayPages = 10; // Số trang hiển thị [1, 2, 3, 4 ,5, 6 ,7, 8 ,9, 10]
      // Tính toán Tổng số trang chia được trên tổng số sản phẩm chúng ta có
      var totalPages = Math.ceil(totalItems / pageSize);
      // Ở đây mình sẽ chia ra 2 case: 
      var startPage, endPage;
      if (totalPages <= displayPages) {
        // Trường hợp chỉ có 1 trang sản phẩm thì mình cần init 1 mảng từ trang 1 đến cuối trang thôi. ez :D
        startPage = 1;
        endPage = totalPages;
      } else {
        // Trường hợp nhiều hơn 1 trang thì sao? Mình cần chia ra 3 case nữa: Số trang gửi lên là nằm ở cuối danh sách hiển thị, Số trang gửi lên nằm ở giữa danh sách hiển thị và Số trang nằm ở đầu danh sách hiển thị
        if (currentPage <= 6) {
          startPage = 1;
          endPage = displayPages;
        } else if (currentPage + 4 >= totalPages) {
          startPage = totalPages - 9;
          endPage = totalPages;
        } else {
          startPage = currentPage - 5;
          endPage = currentPage + 4;
        }
      }

Tóm lại là mình cần init 1 mảng Tịnh - Tiến thích hợp với số trang mình đang thao tác (số trang được gửi lên) Code services phân trang sẽ là:

'use strict';

angular.module('eshopApp')
  .factory('pagerService', ['common', pagerService]);

function pagerService(common) {

  var service = {};
  service.GetPager = GetPager;

  return service;

    function GetPager(totalItems, currentPage, pageSize) {
    // gán mặc định cho pages đang thao tác, nếu không có thì là sẽ là trang 1
      currentPage = currentPage || 1;

      // gán mặc định cho số sản phẩm hiển thị trên trang. Ban đầu nó sẽ là số mình gán, nhưng về trang cuối cùng thì số này sẽ là 1 số <= 6
      pageSize = pageSize || 6;
      var displayPages = 10; // Số trang hiển thị [1, 2, 3, 4 ,5, 6 ,7, 8 ,9, 10]
      // Tính toán Tổng số trang chia được trên tổng số sản phẩm chúng ta có
      var totalPages = Math.ceil(totalItems / pageSize);
      // Ở đây mình sẽ chia ra 2 case: 
      var startPage, endPage;
      if (totalPages <= displayPages) {
        // Trường hợp chỉ có 1 trang sản phẩm thì mình cần init 1 mảng từ trang 1 đến cuối trang thôi. ez :D
        startPage = 1;
        endPage = totalPages;
      } else {
        // Trường hợp nhiều hơn 1 trang thì sao? Mình cần chia ra 3 case nữa: Số trang gửi lên là nằm ở cuối danh sách hiển thị, Số trang gửi lên nằm ở giữa danh sách hiển thị và Số trang nằm ở đầu danh sách hiển thị
        if (currentPage <= 6) {
          startPage = 1;
          endPage = displayPages;
        } else if (currentPage + 4 >= totalPages) {
          startPage = totalPages - 9;
          endPage = totalPages;
        } else {
          startPage = currentPage - 5;
          endPage = currentPage + 4;
        }
      }
      // Tính toán lại index đầu và cuối để chặt mảng Sản phẩm cần hiển thị
      var startIndex = (currentPage - 1) * pageSize;
      var endIndex = Math.min(startIndex + pageSize - 1, totalItems - 1);

      // Mảng số trang sẽ được hiển thị ngoài view
      var pages = _.range(startPage, endPage + 1);

      return {
        totalItems: totalItems,
        currentPage: currentPage,
        pageSize: pageSize,
        displayPages: displayPages,
        totalPages: totalPages,
        startPage: startPage,
        endPage: endPage,
        startIndex: startIndex,
        endIndex: endIndex,
        pages: pages
      };
    }
}

Code View để hiển thị UI phân trang

<div class="col-sm-12">
        <ul ng-if="vm.pager.pages.length" class="pagination">
          <li ng-class="{disabled:vm.pager.currentPage === 1}">
              <a ng-click="vm.setPage(1)" class="link-pagination">First</a>
          </li>
          <li ng-class="{disabled:vm.pager.currentPage === 1}">
              <a ng-click="vm.setPage(vm.pager.currentPage - 1)" class="link-pagination">Previous</a>
          </li>
          <li ng-repeat="page in vm.pager.pages" ng-class="{active:vm.pager.currentPage === page}">
              <a ng-click="vm.setPage(page)" class="link-pagination">{{page}}</a>
          </li>
          <li ng-class="{disabled:vm.pager.currentPage === vm.pager.totalPages}">
              <a ng-click="vm.setPage(vm.pager.currentPage + 1)" class="link-pagination">Next</a>
          </li>
          <li ng-class="{disabled:vm.pager.currentPage === vm.pager.totalPages}">
              <a ng-click="vm.setPage(vm.pager.totalPages)" class="link-pagination">Last</a>
          </li>
        </ul>
      </div>

Controller cũng cần phải thay đổi một chút để reassign lại cho model Ỉtems

vm.initData = function(topRateItems, categories, itemNewest){
    vm.topRateItems = topRateItems;
    vm.categories = categories;
    vm.initPagition(itemNewest);
  };

  vm.filterPrice = function(){
    // var priceSlider = vm.priceSlider;
    var priceSlider = angular.element('.priceslider').val();
    filterItemService.filterByPrice(priceSlider).then(function(res) {
      vm.initPagition(res.newest_items);
    }, function(err){
    });
  }


  vm.filterByCategory = function(categoryID){
    filterItemService.filterByCategory(categoryID).then(function(res) {
      vm.itemNewest = res.newest_items;
    }, function(err){
    });
  };


  vm.initPagition = function(tempItems){
    vm.tempItems = tempItems;
    vm.pager = {};
    vm.setPage = setPage;
    initController();
    function initController() {
      vm.setPage(1);
    }

    function setPage(page) {
      if (page < 1 || page > vm.pager.totalPages) {
        return;
      }
      vm.pager = pagerService.GetPager(vm.tempItems.length, page);
      vm.itemNewest = vm.tempItems.slice(vm.pager.startIndex, vm.pager.endIndex + 1);
    }
  };

Sau một hồi vật lộn chúng ta cũng đã có một kết quả mượt hơn dự tính 😄

3. ## Kết

Hôm nay chúng ta đã đi qua 2 phần nhưng bài khá là dài, mình sẽ tiếp tục Series này ở những tháng sau. Hẹn gặp lại các bạn 😄