├── .gitignore ├── README.md ├── app.js └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angular-faceted-search 2 | ====================== 3 | 4 | An implementation of faceted search of a JSON dataset with AngularJS. 5 | 6 | AngularJS v1.3.14 7 | 8 | References: 9 | 10 | * [This fiddle](http://jsfiddle.net/rzgWr/19/) 11 | * [JS Objects: De"construct"ion](http://davidwalsh.name/javascript-objects-deconstruction) 12 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Reference source - http://jsfiddle.net/rzgWr/19/ 4 | 5 | var myApp = angular.module('myApp',[]); 6 | 7 | //---------------------------------------- HELPERS FACTORY 8 | 9 | myApp.factory('Helpers', function() { 10 | return { 11 | uniq: function(data, key) { 12 | var result = []; 13 | 14 | for (var i = 0; i < data.length; i++) { 15 | var value = data[i][key]; 16 | 17 | if (result.indexOf(value) == -1) { 18 | result.push(value); 19 | } 20 | } 21 | return result; 22 | }, 23 | contains: function(data, obj) { 24 | for (var i = 0; i < data.length; i++) { 25 | if (data[i] === obj) { 26 | return true; 27 | } 28 | } 29 | return false; 30 | } 31 | }; 32 | }); 33 | 34 | //---------------------------------------- CONTROLLER 35 | 36 | myApp.controller('MainCtrl', function($scope, Helpers) { 37 | $scope.useFacets = {}; 38 | 39 | $scope.items = [ 40 | { 41 | id: 1, 42 | name: 'Brick 1 x 1', 43 | type: 'brick', 44 | color: 'red', 45 | studs: 1 46 | }, 47 | { 48 | id: 2, 49 | name: 'Brick 3 x 2', 50 | type: 'brick', 51 | color: 'green', 52 | studs: 6 53 | }, 54 | { 55 | id: 3, 56 | name: 'Brick 2 x 2', 57 | type: 'brick', 58 | color: 'red', 59 | studs: 4 60 | }, 61 | { 62 | id: 4, 63 | name: 'Brick 2 x 1', 64 | type: 'brick', 65 | color: 'blue', 66 | studs: 2 67 | }, 68 | { 69 | id: 5, 70 | name: 'Plate 6 x 4', 71 | type: 'plate', 72 | color: 'yellow', 73 | studs: 24 74 | }, 75 | { 76 | id: 6, 77 | name: 'Plate 3 x 2', 78 | type: 'plate', 79 | color: 'blue', 80 | studs: 6 81 | }, 82 | { 83 | id: 7, 84 | name: 'Plate 2 x 2', 85 | type: 'plate', 86 | color: 'green', 87 | studs: 4 88 | }, 89 | { 90 | id: 8, 91 | name: 'Plate 2 x 2', 92 | type: 'plate', 93 | color: 'red', 94 | studs: 4 95 | }, 96 | { 97 | id: 9, 98 | name: 'Tile 2 x 1', 99 | type: 'tile', 100 | color: 'yellow', 101 | studs: 0 102 | }, 103 | { 104 | id: 10, 105 | name: 'Tile 2 x 2', 106 | type: 'tile', 107 | color: 'decorated', 108 | studs: 0 109 | }, 110 | { 111 | id: 11, 112 | name: 'Tile 3 x 2', 113 | type: 'tile', 114 | color: 'decorated', 115 | studs: 0 116 | }, 117 | { 118 | id: 12, 119 | name: 'Tile 2 x 1', 120 | type: 'tile', 121 | color: 'yellow', 122 | studs: 0 123 | } 124 | ]; 125 | 126 | $scope.count = function (prop, value) { 127 | return function (el) { 128 | return el[prop] == value; 129 | }; 130 | }; 131 | 132 | // Sort the available facets alphabetically/numerically. 133 | // http://stackoverflow.com/a/18261306 134 | $scope.orderByValue = function(value) { 135 | return value; 136 | }; 137 | 138 | /*--- 139 | FACET MANIPULATION FUNCTIONS 140 | - clear facets, search 141 | - FacetResults constructor 142 | ---*/ 143 | 144 | // Reset all previously-selected facets. 145 | $scope.clearAllFacets = function() { 146 | $scope.activeFacets = []; 147 | $scope.useFacets = {}; 148 | }; 149 | 150 | // Clear search query. 151 | $scope.clearQuery = function() { 152 | $scope.query = null; 153 | }; 154 | 155 | // Clear a specific facet. 156 | $scope.clearFacet = function(facet) { 157 | // Find the index of the facet so we can remove it from the active facets. 158 | var i = $scope.activeFacets.indexOf(facet); 159 | 160 | // If it exists, remove the facet from the list of active facets. 161 | if (i != -1) { 162 | $scope.activeFacets.splice(i, 1); 163 | 164 | // Find the corresponding facet in the filter models and turn it off. 165 | for (var k in $scope.useFacets) { 166 | if ($scope.useFacets[k]) { 167 | $scope.useFacets[k][facet] = false; 168 | } 169 | } 170 | } 171 | }; 172 | 173 | // Clear any active facets when a search query is entered (or cleared). 174 | // Add newValue && (!!oldValue === false) to if statement to allow search query to be changed and preserve facets. 175 | $scope.$watch('query', function (newValue, oldValue) { 176 | if ((newValue !== oldValue) && $scope.activeFacets.length) { 177 | $scope.clearAllFacets(); 178 | } 179 | }); 180 | 181 | var filterAfters = []; 182 | 183 | // FacetResults "constructor" object. 184 | // http://davidwalsh.name/javascript-objects-deconstruction 185 | var FacetResults = { 186 | init: function(facetIndex, facetName) { 187 | this.facetIndex = facetIndex; 188 | this.facetName = facetName; 189 | }, 190 | filterItems: function(filterAfterArray) { 191 | // Name the new array created after filter is run. 192 | var newArrayIndex = this.facetIndex; 193 | // Add the new array to the filterAfters array 194 | filterAfters[newArrayIndex] = []; 195 | 196 | var selected = false; 197 | 198 | // Iterate over previously filtered items. 199 | for (var n in filterAfterArray) { 200 | var itemObj = filterAfterArray[n], 201 | useFacet = $scope.useFacets[this.facetName]; 202 | 203 | // Iterate over new facet. 204 | for (var facet in useFacet) { 205 | if (useFacet[facet]) { 206 | selected = true; 207 | 208 | // Push facet to list of active facets if doesn't already exist. 209 | if (!Helpers.contains($scope.activeFacets, facet)) { 210 | $scope.activeFacets.push(facet); 211 | } 212 | 213 | // Push item from previous filter to new array if matches new facet and unique. 214 | // (Using == instead of === enables matching integers to strings) 215 | if (itemObj[this.facetName] == facet && !Helpers.contains(filterAfters[newArrayIndex], itemObj)) { 216 | filterAfters[newArrayIndex].push(itemObj); 217 | break; 218 | } 219 | } else { 220 | selected = false; 221 | 222 | // Remove facet from list of active facets if toggled off. 223 | var facetIndex = $scope.activeFacets.indexOf(facet); 224 | 225 | if (facetIndex > -1) { 226 | $scope.activeFacets.splice(facetIndex, 1); 227 | } 228 | } 229 | } 230 | } 231 | 232 | if (!selected) { 233 | filterAfters[newArrayIndex] = filterAfterArray; 234 | } 235 | } 236 | }; 237 | 238 | /*--- 239 | SET UP FACETS 240 | - define facet group names 241 | - compile all facets that belong in each group 242 | - create new objects for each set of facet results 243 | ---*/ 244 | 245 | // Define the facet group names. 246 | // If fetching from data, this will need to be in resolve/callback. 247 | var facetGroupNames = ['type', 'color', 'studs']; 248 | var facetGroupNamesLen = facetGroupNames.length; 249 | $scope.facetGroups = []; 250 | 251 | // Collect all options for each facet group from items dataset. 252 | // The HTML template will iterate over the facetGroups array to generate filter options. 253 | // (Alternately, we could pre-define the facets we want to use) 254 | for (var i = 0; i < facetGroupNamesLen; i++) { 255 | var facetGroupObj = { 256 | name: facetGroupNames[i], 257 | facets: Helpers.uniq($scope.items, facetGroupNames[i]) 258 | }; 259 | 260 | $scope.facetGroups.push(facetGroupObj); 261 | } 262 | 263 | // Create new object for each set of facet results (ie., like "new"ing). 264 | var filterBy = []; 265 | 266 | for (var i = 0; i < facetGroupNamesLen; i++) { 267 | var thisName = facetGroupNames[i]; 268 | 269 | filterBy.push(Object.create(FacetResults)); 270 | filterBy[i].init(i, thisName); 271 | } 272 | 273 | /*--- 274 | WATCH FACET SELECTION 275 | - "new" each facet results set 276 | - run filters 277 | - return final list of items after last filter is run 278 | ---*/ 279 | 280 | $scope.activeFacets = []; 281 | 282 | $scope.$watch('useFacets', function(newVal, oldVal) { 283 | // Filter each facet set. 284 | for (var i = 0; i < facetGroupNamesLen; i++) { 285 | if (i === 0) { 286 | filterBy[0].filterItems($scope.items); 287 | } else { 288 | filterBy[i].filterItems(filterAfters[i - 1]); 289 | } 290 | } 291 | 292 | // Return the final filtered list of items. 293 | $scope.filteredItems = filterAfters[facetGroupNamesLen - 1]; 294 | 295 | }, true); 296 | }); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ng-faceted-search 6 | 7 | 72 | 73 | 74 | 75 | 76 |
77 | 78 |
79 | Search: × 80 |
81 | 82 |
83 |
84 |

{{group.name}}

85 |
86 |
87 | 88 | 89 |
90 |
91 |
92 |
93 | 94 |
95 |
96 |

{{(filteredItems | filter:query).length}} Item{{(filteredItems | filter:query).length !== 1 ? 's' : null}}

97 | 98 |

99 | Clear All 100 | {{facet}} 101 |

102 | 103 |
    104 |
  • 105 |

    {{item.name}}

    106 |

    Type: {{item.type}}
    107 | Color: {{item.color}}
    108 | Studs: {{item.studs}}

    109 |
  • 110 |
111 |

Sorry, no results found!

112 |
113 |
114 | 115 |
116 | 117 | 118 | 119 | 120 | --------------------------------------------------------------------------------