Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS toggle button filters

So this is a javascript conversion from jQuery app into Angular app. The current jQuery app works but is needed to make into a real app using the Angular framework.

The logic behind the whole app is to select categories and filter OUT and get specific results based on the filter buttons. So lets say you want only to see results that include only where filter 1 AND filter 2 are together, but not (filter1, filter2, and filter1+filter2). see the jquery version: demo

$(document).ready(function(){
    $('.filter-selector').click(function(){

        /* Filter 1 Categories */
        if($(this).attr("value")==".filter1_group1"){
            $(".filter1_group1-show").toggle();
            $(this).toggleClass('active');
        }
        if($(this).attr("value")==".filter1_group2"){
            $(".filter1_group2-show").toggle();
            $(this).toggleClass('active');
        }
    });
});

Now I need to convert that javascript magic over to angular, keep the buttons in a toggle stat and show results on the second view. It will essentially be an Angular SPA with 2 views: 1 for filters and 1 for results. Previous app was using jQuery toggle class function, but there is no built in function for Angular in this case. All the examples for toggle button for Angular have only 1 toggle button that hides/shows divs. And other example buttons only show or hide divs separately and are not toggle buttons. And how do I turn filter results into service return and then inject that into View 2 as results and show them?

Need some direction from Angular gods here...

UPDATE 1: thanx to Shaun Scovil, the Angular way of creating this filter groups was found. However the filter group works well on a single page but not in 2 view SPA app: plunkr The filters will break after switching between filters and cases a few times.

UPDATE 2: thanx to Shaun Scovil once more, the filters/cases toggle buttons work now going from page view to page view back to any number of views: plunkr

like image 733
AivoK Avatar asked Jan 28 '16 19:01

AivoK


1 Answers

Based on your example app and description, here is how I would describe what you need in Angular terms:

  • a controller for your filter toggles view
  • a controller for your cases view
  • a service to store toggled filters
  • a directive for your filter toggle buttons
  • a filter to reduce the list of cases by toggled filters

Working example: JSFiddle (UPDATED to work with ngRoute)

Controllers

The two controllers should serve as view models, providing some well-formed data that can be used in their respective view templates. For example:

angular.module('myApp')
  .controller('FilterToggleController', FilterToggleController)
  .controller('CasesController', CasesController)
;

function FilterToggleController() {
  var vm = this;
  vm.filterGroups = {
    1: [1,2],
    2: [1,2]
  };
}

function CasesController() {
  var vm = this;
  vm.cases = [
    {label:'Case 1,2', filters:[{group:1, filter:1}, {group:1, filter: 2}]},
    {label:'Case 1',   filters:[{group:1, filter:1}]},
    {label:'Case 2',   filters:[{group:1, filter:2}]},
    {label:'Case 1,3', filters:[{group:1, filter:1}, {group:2, filter:1}]},
    {label:'Case 4',   filters:[{group:2, filter:2}]}
  ];
}

Service

The purpose of an Angular service is to share data or functionality among controllers, directives, filters and other services. Your service is a data store for the selected filters, so I would use a $cacheFactory cache under the hood. For example:

angular.module('myApp')
  .factory('$filterCache', filterCacheFactory)
;

function filterCacheFactory($cacheFactory) {
  var cache = $cacheFactory('filterCache');
  var $filterCache = {};

  $filterCache.has = function(group, filter) {
    return cache.get(concat(group, filter)) === true;
  };

  $filterCache.put = function(group, filter) {
    cache.put(concat(group, filter), true);
  }

  $filterCache.remove = function(group, filter) {
    cache.remove(concat(group, filter));
  }

  $filterCache.count = function() {
    return cache.info().size;
  }

  function concat(group, filter) {
    return group + ':' + filter;
  }

  return $filterCache;
}

Directive

A directive adds functionality to an HTML element. In your case, I would create a directive with a 'click' event handler that can be added as an attribute to a button or any other element. Our $filterCache service could be used by the event handler to keep track of the group/filter combination that the button represents. For example:

angular.module('myApp')
  .directive('toggleFilter', toggleFilterDirective)
;

function toggleFilterDirective($filterCache) {
  return function(scope, iElement, iAttrs) {
    var toggled = false;

    iElement.on('click', function() {
      var group = scope.$eval(iAttrs.group);
      var filter = scope.$eval(iAttrs.filter);

      toggled = !toggled;

      if (toggled) {
        $filterCache.put(group, filter);
        iElement.addClass('toggled');
      } else {
        $filterCache.remove(group, filter);
        iElement.removeClass('toggled');
      }

      scope.$apply();
    });
  };
}

Filter

The purpose of the filter is to take the array of case objects defined in CasesController and reduce them based on the filters stored in our $filterCache service. It will reduce the list to an empty array if no filters are toggled. For example:

angular.module('myApp')
  .filter('filterCases', filterCasesFactory)
;

function filterCasesFactory($filterCache) {
  return function(items) {
    var filteredItems = [];
    var filterCount = $filterCache.count();

    if (filterCount) {
      angular.forEach(items, function(item) {
        if (angular.isArray(item.filters) && item.filters.length >= filterCount) {
          for (var matches = 0, i = 0; i < item.filters.length; i++) {
            var group = item.filters[i].group;
            var filter = item.filters[i].filter;

            if ($filterCache.has(group, filter))
              matches++;

            if (matches === filterCount) {
              filteredItems.push(item);
              break;
            }
          }
        }
      });
    }

    return filteredItems;
  };
}

Template

Finally, the HTML template ties it all together. Here is an example of how that would look using all of the other pieces we've built:

<!-- Filter Toggles View -->
<div ng-controller="FilterToggleController as vm">
  <div ng-repeat="(group, filters) in vm.filterGroups">
    <h2>
      Group {{group}}
    </h2>
    <div ng-repeat="filter in filters">
      <button toggle-filter group="group" filter="filter">
        Filter {{filter}}
      </button>
    </div>
  </div>
</div>

<!-- Cases View -->
<div ng-controller="CasesController as vm">
  <h2>
    Your Cases
  </h2>
  <ol>
    <li ng-repeat="case in vm.cases | filterCases">
      {{case.label}}
    </li>
  </ol>
</div>

UPDATE

Based on the comments, I updated the JSFiddle example to work with ngRoute by making the following changes to the toggleFilterDirective:

function toggleFilterDirective($filterCache) {
  return function(scope, iElement, iAttrs) {
    var group, filter, toggled;
    sync();
    update();
    iElement.on('click', onClick);
    scope.$on('$destroy', offClick);

    function onClick() {
      sync();
      toggle();
      update();
      scope.$apply();
    }

    function offClick() {
      iElement.off('click', onClick);
    }

    function sync() {
      group = scope.$eval(iAttrs.group);
      filter = scope.$eval(iAttrs.filter);
      toggled = $filterCache.has(group, filter);
    }

    function toggle() {
      toggled = !toggled;
      if (toggled) {
        $filterCache.put(group, filter);
      } else {
        $filterCache.remove(group, filter);
      }
    }

    function update() {
      if (toggled) {
        iElement.addClass('toggled');
      } else {
        iElement.removeClass('toggled');
      }
    }
  };
}

Here is a link to the original example: JSFiddle

like image 115
Shaun Scovil Avatar answered Nov 04 '22 19:11

Shaun Scovil