├── LICENSE ├── .versions ├── smart.json ├── package.js ├── filter-collections-server.js ├── filter-collections-client.js └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | base64@1.0.4 2 | ejson@1.0.7 3 | julianmontagna:filter-collections@1.0.3 4 | meteor@1.1.10 5 | underscore@1.0.4 6 | -------------------------------------------------------------------------------- /smart.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "filter-collections", 3 | "description": "Smart package for Meteor that adds Sorting, Paging, Filter and Search capabilities for our collections.", 4 | "homepage": "https://github.com/julianmontagna/filter-collections", 5 | "author": "Julian Montagna (http://www.tooit.com)", 6 | "version": "0.1.4", 7 | "git": "https://github.com/julianmontagna/filter-collections.git", 8 | "packages": {} 9 | } 10 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: "tooit:filter-collections", 3 | summary: "Smart package for Meteor that adds filter and pager behavior to our Meteor's collections.", 4 | version: "1.0.3", 5 | git: "https://github.com/julianmontagna/filter-collections" 6 | }); 7 | 8 | Package.onUse(function (api) { 9 | api.versionsFrom(['METEOR@0.9.3', 'METEOR@0.9.4', 'METEOR@1.0']); 10 | 11 | api.use([ 12 | 'underscore', 13 | 'ejson' 14 | ], [ 15 | 'client', 16 | 'server' 17 | ]); 18 | 19 | api.addFiles('filter-collections-client.js', ['client']); 20 | api.addFiles('filter-collections-server.js', ['server']); 21 | api.export('FilterCollections') 22 | }); 23 | -------------------------------------------------------------------------------- /filter-collections-server.js: -------------------------------------------------------------------------------- 1 | FilterCollections = {}; 2 | 3 | FilterCollections.publish = function (collection, options) { 4 | 5 | var self = this; 6 | 7 | options = options || {}; 8 | 9 | var callbacks = options.callbacks || {}; 10 | 11 | // var cursor = {}; 12 | 13 | var name = (options.name) ? options.name : collection._name; 14 | 15 | var publisherResultsId = 'fc-' + name + '-results'; 16 | var publisherCountId = 'fc-' + name + '-count'; 17 | var publisherCountCollectionName = name + 'CountFC'; 18 | 19 | /** 20 | * Publish query results. 21 | */ 22 | 23 | Meteor.publish(publisherResultsId, function (query) { 24 | 25 | var allow = true; 26 | 27 | if (callbacks.allow && _.isFunction(callbacks.allow)) 28 | allow = callbacks.allow(query, this); 29 | 30 | if(!allow){ 31 | throw new Meteor.Error(417, 'Not allowed'); 32 | } 33 | 34 | query = (query && !_.isEmpty(query)) ? query : {}; 35 | 36 | query.selector = query.selector || {}; 37 | 38 | query.options = query.options || { 39 | sort: [], 40 | skip: 0, 41 | limit: 10 42 | }; 43 | 44 | if (callbacks.beforePublish && _.isFunction(callbacks.beforePublish)) 45 | query = callbacks.beforePublish(query, this) || query; 46 | 47 | var cursor = collection.find(query.selector, query.options); 48 | 49 | if (callbacks.afterPublish && _.isFunction(callbacks.afterPublish)) 50 | cursor = callbacks.afterPublish('results', cursor, this) || cursor; 51 | 52 | return cursor; 53 | }); 54 | 55 | /** 56 | * Publish result count. 57 | */ 58 | 59 | Meteor.publish(publisherCountId, function (query) { 60 | var self = this; 61 | var allow = true; 62 | var cursor = {}; 63 | 64 | if (callbacks.allow && _.isFunction(callbacks.allow)) 65 | allow = callbacks.allow(query, this); 66 | 67 | if(!allow){ 68 | throw new Meteor.Error(417, 'Not allowed'); 69 | } 70 | 71 | query = (query && !_.isEmpty(query)) ? query : {}; 72 | query.selector = query.selector || {}; 73 | 74 | if(callbacks.beforePublish && _.isFunction(callbacks.beforePublish)) 75 | query = callbacks.beforePublish(query, this) || query; 76 | 77 | count = collection.find(query.selector).count() || 0; 78 | 79 | if(callbacks.afterPublish && _.isFunction(callbacks.afterPublish)) 80 | cursor = callbacks.afterPublish('count', cursor, this) || cursor; 81 | 82 | self.added(publisherCountCollectionName, Meteor.uuid(), { 83 | count: count, 84 | query: query 85 | }); 86 | 87 | this.ready(); 88 | }); 89 | }; 90 | -------------------------------------------------------------------------------- /filter-collections-client.js: -------------------------------------------------------------------------------- 1 | FilterCollections = function (collection, settings) { 2 | 3 | var self = this; 4 | 5 | var _settings = settings || {}; 6 | var _initialized = false; 7 | var _EJSONQuery = {}; 8 | 9 | self._collection = collection || {}; 10 | 11 | self.name = (_settings.name) ? _settings.name : self._collection._name; 12 | 13 | var _subscriptionResultsId = 'fc-' + self.name + '-results'; 14 | var _subscriptionCountId = 'fc-' + self.name + '-count'; 15 | 16 | self._collectionCount = new Meteor.Collection(self.name + 'CountFC'); 17 | 18 | var _deps = { 19 | query: new Deps.Dependency(), 20 | sort: new Deps.Dependency(), 21 | pager: new Deps.Dependency(), 22 | filter: new Deps.Dependency(), 23 | search: new Deps.Dependency() 24 | }; 25 | 26 | var _callbacks = { 27 | beforeSubscribe: (_settings.callbacks && _settings.callbacks.beforeSubscribe) ? _settings.callbacks.beforeSubscribe : null, 28 | afterSubscribe: (_settings.callbacks && _settings.callbacks.afterSubscribe) ? _settings.callbacks.afterSubscribe : null, 29 | beforeSubscribeCount: (_settings.callbacks && _settings.callbacks.beforeSubscribeCount) ? _settings.callbacks.beforeSubscribeCount : null, 30 | afterSubscribeCount: (_settings.callbacks && _settings.callbacks.afterSubscribeCount) ? _settings.callbacks.afterSubscribeCount : null, 31 | beforeResults: (_settings.callbacks && _settings.callbacks.beforeResults) ? _settings.callbacks.beforeResults : null, 32 | afterResults: (_settings.callbacks && _settings.callbacks.afterResults) ? _settings.callbacks.afterResults : null, 33 | templateCreated: (_settings.callbacks && _settings.callbacks.templateCreated) ? _settings.callbacks.templateCreated : null, 34 | templateRendered: (_settings.callbacks && _settings.callbacks.templateRendered) ? _settings.callbacks.templateRendered : null, 35 | templateDestroyed: (_settings.callbacks && _settings.callbacks.templateDestroyed) ? _settings.callbacks.templateDestroyed : null, 36 | }; 37 | 38 | var _template = _settings.template || {}; 39 | 40 | var _sorts = (_settings.sort && _settings.sort.defaults) ? _settings.sort.defaults : []; 41 | var _sortOrder = (_settings.sort && _settings.sort.order) ? _settings.sort.order : ['asc', 'desc']; 42 | 43 | var _pager = { 44 | totalItems: 0, 45 | defaultOptions: (_settings.pager && _settings.pager.options) ? _settings.pager.options : [10, 20, 30, 40, 50], 46 | itemsPerPage: (_settings.pager && _settings.pager.itemsPerPage) ? parseInt(_settings.pager.itemsPerPage, 10) : 10, 47 | currentPage: (_settings.pager && _settings.pager.currentPage) ? parseInt(_settings.pager.currentPage, 10) : 1, 48 | showPages: (_settings.pager && _settings.pager.showPages) ? parseInt(_settings.pager.showPages, 10) : 10 49 | }; 50 | 51 | var _filters = _settings.filters || {}; 52 | 53 | var _subs = { 54 | results: {}, 55 | count: {} 56 | }; 57 | 58 | var _query = { 59 | selector: {}, 60 | options: {} 61 | }; 62 | 63 | /** 64 | * [_autorun description] 65 | * @return {[type]} [description] 66 | */ 67 | var _autorun = function () { 68 | 69 | Deps.autorun(function (computation) { 70 | 71 | if (!_initialized) { 72 | self.sort.init(); // Set default query values for sorting. 73 | self.pager.init(); // Set defaul query values for paging. 74 | self.search.init(); // Set default searchable fields. 75 | _initialized = true; 76 | } 77 | 78 | var query = self.query.get(); 79 | 80 | if (_.isFunction(_callbacks.beforeSubscribe)) 81 | query = _callbacks.beforeSubscribe(query) || query; 82 | 83 | _subs.results = Meteor.subscribe(_subscriptionResultsId, query, { 84 | onError: function(error){ 85 | if (_.isFunction(_callbacks.afterSubscribe)) 86 | _callbacks.afterSubscribe(error, this); 87 | } 88 | }); 89 | 90 | if(_subs.results.ready() && _.isFunction(_callbacks.afterSubscribe)) 91 | _callbacks.afterSubscribe(null, this); 92 | 93 | if (_.isFunction(_callbacks.beforeSubscribeCount)) 94 | query = _callbacks.beforeSubscribeCount(query) || query; 95 | 96 | _subs.count = Meteor.subscribe(_subscriptionCountId, query, { 97 | onError: function(error){ 98 | if (_.isFunction(_callbacks.afterSubscribeCount)) 99 | _callbacks.afterSubscribeCount(error, this); 100 | } 101 | }); 102 | 103 | if(_subs.count.ready()){ 104 | 105 | if (_.isFunction(_callbacks.afterSubscribeCount)) 106 | _callbacks.afterSubscribeCount(null, this); 107 | 108 | var res = self._collectionCount.findOne({}); 109 | self.pager.setTotals(res); 110 | } 111 | 112 | }); 113 | 114 | return; 115 | }; 116 | 117 | /** 118 | * [sort description] 119 | * @type {Object} 120 | */ 121 | self.sort = { 122 | init: function(){ 123 | this.run(false); 124 | return; 125 | }, 126 | get: function () { 127 | _deps.sort.depend(); 128 | 129 | var ret = {}; 130 | _.each(_sorts, function (sort) { 131 | for(var parts = sort[0].split('.'), i=0, l=parts.length, cache=ret; i= value){ 277 | selected = (_pager.itemsPerPage === value) ? true : false; 278 | options.unshift({ 279 | value: value, 280 | status: (selected) ? 'selected' : '' 281 | }); 282 | }else{ 283 | appendLast = true; 284 | } 285 | }); 286 | 287 | if (appendLast){ 288 | options.unshift({ 289 | value: totalItems, 290 | status: (selected) ? 'selected' : '' 291 | }); 292 | } 293 | 294 | return options; 295 | }, 296 | getOffsetStart: function () { 297 | var offsetStart = (_pager.currentPage - 1) * _pager.itemsPerPage; 298 | return offsetStart; 299 | }, 300 | getOffsetEnd: function () { 301 | var offsetEnd = this.getOffsetStart() + _pager.itemsPerPage; 302 | return (offsetEnd > _pager.totalItems) ? _pager.totalItems : offsetEnd; 303 | }, 304 | getPages: function () { 305 | var pages = []; 306 | var allPages = []; 307 | 308 | var totalPages = _pager.totalPages; 309 | var currentPage = _pager.currentPage; 310 | var showPages = _pager.showPages; 311 | 312 | var start = (currentPage - 1) - Math.floor(showPages / 2); 313 | if (start < 0) start = 0; 314 | var end = start + showPages; 315 | if (end > totalPages) { 316 | end = totalPages; 317 | start = end - showPages; 318 | if (start < 0) start = 0; 319 | } 320 | 321 | for (var i = start; i < end; i++) { 322 | var status = (currentPage === i + 1) ? 'active' : ''; 323 | pages.push({ 324 | page: i + 1, 325 | status: status 326 | }); 327 | } 328 | 329 | return pages; 330 | }, 331 | setTotals: function(res){ 332 | _pager.totalItems = res.count; 333 | _pager.totalPages = Math.ceil(_pager.totalItems / _pager.itemsPerPage); 334 | self.pager.set(); 335 | }, 336 | hasPrevious: function () { 337 | var hasPrevious = (_pager.currentPage > 1); 338 | return hasPrevious; 339 | }, 340 | hasNext: function () { 341 | var hasNext = (_pager.currentPage < _pager.totalPages); 342 | return hasNext; 343 | }, 344 | moveTo: function (page) { 345 | if (_pager.currentPage !== page) { 346 | _pager.currentPage = page; 347 | self.pager.set(true); 348 | } 349 | 350 | return; 351 | }, 352 | movePrevious: function () { 353 | if (this.hasPrevious()) { 354 | _pager.currentPage--; 355 | this.set(true); 356 | } 357 | 358 | return; 359 | }, 360 | moveFirst: function () { 361 | if (this.hasPrevious()) { 362 | _pager.currentPage = 1; 363 | this.set(true); 364 | } 365 | 366 | return; 367 | }, 368 | moveNext: function () { 369 | if (this.hasNext()) { 370 | _pager.currentPage++; 371 | this.set(true); 372 | } 373 | 374 | return; 375 | }, 376 | moveLast: function () { 377 | if (this.hasNext()) { 378 | _pager.currentPage = _pager.totalPages; 379 | this.set(true); 380 | } 381 | 382 | return; 383 | } 384 | }; 385 | 386 | /** 387 | * [filter description] 388 | * @type {Object} 389 | */ 390 | self.filter = { 391 | get: function () { 392 | var filters = _filters; 393 | return filters; 394 | }, 395 | set: function (key, filter, triggerUpdate) { 396 | 397 | triggerUpdate = triggerUpdate || true; 398 | 399 | if (!_.has(_filters, key)) 400 | throw new Error("Filter Collection Error: " + key + " is not a valid filter."); 401 | 402 | _filters[key] = _.extend(_filters[key], filter); 403 | 404 | _filters[key].active = _filters[key].active ? false : true; 405 | 406 | if(triggerUpdate) 407 | this.run(); 408 | 409 | return; 410 | }, 411 | getSelector: function(){ 412 | var selector = {}; 413 | var condition = {}; 414 | 415 | _.each(_filters, function (filter, key) { 416 | 417 | if (filter.value) { 418 | 419 | var segment = {}; 420 | var append = {}; 421 | var value; 422 | segment[key] = {}; 423 | 424 | if(filter.value && filter.transform && _.isFunction(filter.transform)) 425 | value = filter.transform(filter.value); 426 | else 427 | value = filter.value; 428 | 429 | if (filter.operator && filter.operator[0]) { 430 | segment[key][filter.operator[0]] = value; 431 | if (filter.operator[1]) 432 | segment[key].$options = filter.operator[1]; 433 | } else { 434 | segment[key] = value; 435 | } 436 | 437 | if (!_.isEmpty(filter.condition)) { 438 | condition[filter.condition] = condition[filter.condition] || []; 439 | condition[filter.condition].push(segment); 440 | } 441 | 442 | if(filter.sort && _.indexOf(_sortOrder, filter.sort) !== -1){ 443 | self.sort.clear(true); 444 | self.sort.set(key, filter.sort, true); 445 | } 446 | 447 | append = (!_.isEmpty(condition)) ? condition : segment; 448 | selector = _.extend(selector, append); 449 | } 450 | }); 451 | return selector; 452 | }, 453 | getActive: function(){ 454 | var filters = []; 455 | 456 | _.each(_filters, function (filter, key) { 457 | if (filter.value) 458 | filters.push({ 459 | title: filter.title, 460 | operator: (filter.operator && filter.operator[0]) ? filter.operator[0] : 'match', 461 | value: filter.value, 462 | key: key 463 | }); 464 | }); 465 | 466 | return filters; 467 | }, 468 | isActive: function(field, value, operator){ 469 | var filters = self.filter.get(); 470 | 471 | if(_.has(filters, field)){ 472 | var check = filters[field]; 473 | 474 | if(!check.value || check.value != value) 475 | return false; 476 | 477 | if(check.operator && check.operator[0]){ 478 | if(check.operator[0] != operator) 479 | return false; 480 | } 481 | 482 | return true; 483 | } 484 | return false; 485 | }, 486 | run: function(){ 487 | _query.selector = this.getSelector(); 488 | self.query.set(_query); 489 | self.pager.moveTo(1); 490 | 491 | return; 492 | }, 493 | clear: function(key, triggerUpdate){ 494 | 495 | triggerUpdate = triggerUpdate || true; 496 | 497 | if(key && _filters[key] && _filters[key].value){ 498 | _filters[key] = _.omit(_filters[key], 'value'); 499 | }else{ 500 | _.each(_filters, function (filter, key) { 501 | if (filter.value) 502 | _filters[key] = _.omit(_filters[key], 'value'); 503 | }); 504 | } 505 | 506 | if(triggerUpdate) 507 | this.run(); 508 | } 509 | }; 510 | 511 | /** 512 | * [search description] 513 | * @type {Object} 514 | */ 515 | self.search = { 516 | criteria: "", 517 | fields: [], 518 | required: [], 519 | init: function(){ 520 | this.setFields(); 521 | return; 522 | }, 523 | getFields: function(full){ 524 | _deps.search.depend(); 525 | 526 | full = full || false; 527 | 528 | if(full) 529 | return _.union(this.fields, this.required); 530 | else 531 | return this.fields; 532 | }, 533 | setFields: function(){ 534 | var _this = this; 535 | var activeSearch = []; 536 | var requiredSearch = []; 537 | 538 | _.each(_filters, function(field, key){ 539 | if(field.searchable && field.searchable === 'optional'){ 540 | activeSearch.push({ 541 | field: key, 542 | title: field.title, 543 | active: false 544 | }); 545 | } 546 | 547 | if(field.searchable && field.searchable === 'required'){ 548 | requiredSearch.push({ 549 | field: key, 550 | title: field.title, 551 | active: true 552 | }); 553 | } 554 | }); 555 | 556 | this.fields = activeSearch; 557 | this.required = requiredSearch; 558 | 559 | return; 560 | }, 561 | setField: function(key){ 562 | var _this = this; 563 | _.each(this.fields, function(field, idx){ 564 | if(_this.fields[idx].field === key && _filters[field.field] && _filters[field.field].searchable !== 'required') 565 | _this.fields[idx].active = (_this.fields[idx].active === true) ? false : true; 566 | }); 567 | 568 | _deps.search.changed(); 569 | return; 570 | }, 571 | setCriteria: function(value, triggerUpdate){ 572 | 573 | triggerUpdate = triggerUpdate || false; 574 | 575 | var activeFields = this.getFields(true); 576 | 577 | if(value){ 578 | this.criteria = value; 579 | _.each(activeFields, function (field, key) { 580 | if (field.active){ 581 | self.filter.set(field.field, { 582 | value: value 583 | }); 584 | } 585 | }); 586 | 587 | if(triggerUpdate) 588 | this.run(); 589 | } 590 | 591 | return; 592 | }, 593 | getCriteria: function(){ 594 | var criteria = this.criteria; 595 | return criteria; 596 | }, 597 | run: function(){ 598 | self.pager.moveTo(1); 599 | return; 600 | }, 601 | clear: function(){ 602 | this.criteria = ""; 603 | self.filter.clear(); 604 | return; 605 | } 606 | }; 607 | 608 | /** 609 | * [query description] 610 | * @type {Object} 611 | */ 612 | self.query = { 613 | get: function () { 614 | _deps.query.depend(); 615 | return EJSON.parse(_EJSONQuery); 616 | }, 617 | set: function (query) { 618 | _EJSONQuery = EJSON.stringify(query); 619 | _deps.query.changed(); 620 | return; 621 | }, 622 | updateResults: function(){ 623 | _query.force = new Date().getTime(); 624 | this.set(_query); 625 | }, 626 | getResults: function(){ 627 | var q = _.clone(_query); 628 | q.options = _.omit(q.options, 'skip', 'limit'); 629 | 630 | if (_.isFunction(_callbacks.beforeResults)) 631 | q = _callbacks.beforeResults(q) || q; 632 | 633 | var cursor = self._collection.find(q.selector, q.options); 634 | 635 | if (_.isFunction(_callbacks.afterResults)) 636 | cursor = _callbacks.afterResults(cursor) || cursor; 637 | 638 | return cursor; 639 | } 640 | }; 641 | 642 | /** 643 | * Template extensions 644 | */ 645 | 646 | if (Template[_template]) { 647 | 648 | Template[_template].created = function () { 649 | _autorun(); 650 | 651 | if (_.isFunction(_callbacks.templateCreated)) 652 | _callbacks.templateCreated(this); 653 | 654 | return; 655 | }; 656 | 657 | Template[_template].rendered = function () { 658 | 659 | if (_.isFunction(_callbacks.templateRendered)) 660 | _callbacks.templateRendered(this); 661 | 662 | return; 663 | }; 664 | 665 | /** Template cleanup. **/ 666 | Template[_template].destroyed = function () { 667 | _subs.results.stop(); 668 | _subs.count.stop(); 669 | 670 | if (_.isFunction(_callbacks.templateDestroyed)) 671 | _callbacks.templateDestroyed(this); 672 | }; 673 | 674 | Template[_template].helpers({ 675 | fcResults: function(){ 676 | return self.query.getResults(); 677 | }, 678 | fcSort: function(){ 679 | return self.sort.get(); 680 | }, 681 | fcPager: function(){ 682 | return self.pager.get(); 683 | }, 684 | fcFilter: function(){ 685 | return self.filter.get(); 686 | }, 687 | fcFilterActive: function(){ 688 | return self.filter.getActive(); 689 | }, 690 | fcFilterSearchable: function(){ 691 | return { 692 | available: self.search.getFields(), 693 | criteria: self.search.getCriteria() 694 | }; 695 | }, 696 | fcFilterObj: function(){ 697 | return self.filter; 698 | }, 699 | fcPagerObj: function(){ 700 | return self.pager; 701 | } 702 | }); 703 | 704 | /** Template events. **/ 705 | Template[_template].events({ 706 | 707 | /** Filters **/ 708 | 'click .fc-filter': function (event) { 709 | event.preventDefault(); 710 | 711 | var field = event.currentTarget.getAttribute('data-fc-filter-field') || false; 712 | var value = event.currentTarget.getAttribute('data-fc-filter-value') || false; 713 | var operator = event.currentTarget.getAttribute('data-fc-filter-operator') || false; 714 | var options = event.currentTarget.getAttribute('data-fc-filter-options') || false; 715 | var sort = event.currentTarget.getAttribute('data-fc-filter-sort') || false; 716 | 717 | var filter = {}; 718 | 719 | if (field && value) 720 | filter['value'] = value; 721 | 722 | if (operator) 723 | filter['operator'] = [operator, options]; 724 | 725 | if (sort) 726 | filter['sort'] = sort; 727 | 728 | self.filter.set(field, filter); 729 | }, 730 | 'click .fc-filter-clear': function (event) { 731 | event.preventDefault(); 732 | 733 | if (self.filter.getActive().length ===1) 734 | self.search.clear(); 735 | 736 | if (_filters[this.key]) 737 | self.filter.clear(this.key); 738 | }, 739 | 'click .fc-filter-reset': function (event) { 740 | event.preventDefault(); 741 | 742 | if (self.filter.getActive().length){ 743 | self.search.clear(); 744 | self.filter.clear(); 745 | } 746 | }, 747 | 748 | /** Search **/ 749 | 'click .fc-search-trigger': function (event, template) { 750 | event.preventDefault(); 751 | 752 | var target = event.currentTarget.getAttribute('data-fc-search-trigger'); 753 | var value = template.find('[data-fc-search-target="'+target+'"]').value || ''; 754 | self.search.setCriteria(value, true); 755 | }, 756 | 'click .fc-search-fields': function (event, template) { 757 | event.preventDefault(); 758 | self.search.setField(this.field); 759 | }, 760 | 'click .fc-search-clear': function (event, template) { 761 | event.preventDefault(); 762 | self.search.clear(); 763 | }, 764 | 765 | /** Pager **/ 766 | 'change .fc-pager-options': function (event) { 767 | event.preventDefault(); 768 | var itemsPerPage = parseInt(event.target.value, 10) || _pager.itemsPerPage; 769 | self.pager.setItemsPerPage(itemsPerPage); 770 | self.pager.setCurrentPage(1, true); 771 | }, 772 | 'click .fc-pager-option': function (event) { 773 | event.preventDefault(); 774 | var itemsPerPage = parseInt(event.currentTarget.getAttribute('data-fc-pager-page'), 10) || _pager.itemsPerPage; 775 | self.pager.setItemsPerPage(itemsPerPage); 776 | self.pager.setCurrentPage(1, true); 777 | }, 778 | 'click .fc-pager-page': function (event) { 779 | event.preventDefault(); 780 | var page = parseInt(event.currentTarget.getAttribute('data-fc-pager-page'), 10) || _pager.currentPage; 781 | self.pager.moveTo(page); 782 | }, 783 | 'click .fc-pager-first': function (event) { 784 | event.preventDefault(); 785 | self.pager.moveFirst(); 786 | }, 787 | 'click .fc-pager-previous': function (event) { 788 | event.preventDefault(); 789 | self.pager.movePrevious(); 790 | }, 791 | 'click .fc-pager-next': function (event) { 792 | event.preventDefault(); 793 | self.pager.moveNext(); 794 | }, 795 | 'click .fc-pager-last': function (event) { 796 | event.preventDefault(); 797 | self.pager.moveLast(); 798 | }, 799 | 800 | /** Sort **/ 801 | 'click .fc-sort': function (event, template) { 802 | event.preventDefault(); 803 | var field = event.currentTarget.getAttribute('data-fc-sort'); 804 | self.sort.set(field, null, true); 805 | }, 806 | 'click .fc-sort-clear': function (event, template) { 807 | event.preventDefault(); 808 | self.sort.clear(); 809 | } 810 | }); 811 | } else { 812 | _autorun(); 813 | } 814 | }; 815 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Meteor's Filter Collections 2 | 3 | Filter Collections is a Smart package for Meteor that adds Sorting, Paging, Filter and Search capabilities for our collections. 4 | Works well (but not necessarily) with [Collection2](https://github.com/aldeed/meteor-collection2 "Collection2"). 5 | 6 | ## Features 7 | 8 | ### Sort 9 | Order results by single or multiple collection's fields. 10 | 11 | ### Pager 12 | Manipulate Meteor's Collection results with a classic pager and items per page results. 13 | 14 | ### Filter 15 | Manage subscribe/publication methods smartly, considering collections with very long datasets avoiding to send the entire collection to the client. 16 | 17 | ### Search 18 | Filtering capabilities also let us build basic and complex search areas and perform simple and multiple field search operations. 19 | 20 | ### Queries 21 | Use package methods to build your own queries and manage results sorted, paginated and filtered. 22 | 23 | ### Template helpers 24 | This module does not attach any template. Instead, it provides useful helpers to work with. 25 | 26 | ## Install 27 | 28 | ### From atmosphere.meteor.com 29 | ``` 30 | mrt add filter-collections 31 | ``` 32 | 33 | ### From github.com 34 | ``` 35 | git clone https://github.com/julianmontagna/filter-collections.git 36 | ``` 37 | 38 | ## Application Example 39 | 40 | [http://filter-collections-example.meteor.com/](http://filter-collections-example.meteor.com/) 41 | 42 | ## Usage 43 | 44 | Considering the "People" Collection created: 45 | ```javascript 46 | People = new Meteor.Collection("people") 47 | ``` 48 | Or with [Collection2](https://github.com/aldeed/meteor-collection2 "Collection2") 49 | ```javascript 50 | People = new Meteor.Collection2("people", {...schema...}); 51 | ``` 52 | 53 | ### Meteor Server side 54 | This package will handle its own publishers (server side) and subscribers (client side) so let's start adding needed configuration on the server. 55 | ```javascript 56 | Meteor.FilterCollections.publish(People, { 57 | name: 'someName', 58 | callbacks: {/*...*/} 59 | }); 60 | ``` 61 | 62 | ### Meteor Client side 63 | Now let's add your collection configuration anywhere you need on the client side. 64 | ```javascript 65 | PeopleFilter = new Meteor.FilterCollections(People, { 66 | template: 'peopleList' 67 | // Other arguments explained later. See Configuration. 68 | }); 69 | ``` 70 | **template**: (optional) a valid template name where to attach package helpers. If not specified, you are still capable of using package methods manually. 71 | 72 | **name**: (optional) setting a name to a Filter Collection instance, let you have multiple instances of Filters sharing the same Collection. If it's specified, the same value should be used on Filter Collection`s publisher methods. 73 | 74 | Then in your html you will have available **fcResults** helper to iterate for: 75 | 76 | ```html 77 | 78 | ... 79 | {{#each fcResults}} 80 | 81 | 82 | 83 | 84 | 85 | 86 | {{/each}} 87 | ... 88 |
{{alias}}{{name}}{{mail}}{{created_at}}
89 | ``` 90 | 91 | With this basic setup you will have the package working for People's Collection. 92 | 93 | ## Configuration 94 | Let's see some package configuration. 95 | 96 | * [Sorting](#sorting) 97 | * [Paginating](#paginating) 98 | * [Filtering](#filtering) 99 | * [Searching](#searching) 100 | * [Queries](#queries) 101 | * [Callbacks](#callbacks) 102 | 103 | # Sorting 104 | 105 | This package lets you sort results in an easy way. You can sort by one or multiple fields at a time and each one will have three states: `null` (not sorted), `'asc'` (ascending) or `'desc'` (descending). For more information see [Specifiers](http://docs.meteor.com/#sortspecifiers "Specifiers"). 106 | 107 | You can provide default collection sorting with the following: 108 | 109 | ```javascript 110 | PeopleFilter = new Meteor.FilterCollections(People, { 111 | ... 112 | sort:{ 113 | order: ['desc', 'asc'], 114 | defaults: [ 115 | ['created_at', 'desc'], 116 | ['company', 'asc'], 117 | ['name', 'asc'], 118 | ] 119 | }, 120 | ... 121 | }); 122 | ``` 123 | 124 | **order**: (optional) by default, the order values are `['asc', 'desc']` but if needed, you can set `['desc', 'asc']` so the states will be `null`, `'desc'`, and `'asc'`. 125 | 126 | **defaults**: (optional) if you need to load the results in a certain order when the collection is first loaded, this is the place. 127 | 128 | *Note: If none of these are specified, default (mongodb) sort order will be provided and you will capable anyway to sort your results later with DOM elements o package methods.* 129 | 130 | ## Templates helpers 131 | 132 | The CSS class *fc-sort* indicates that the package will sort the collection results by *data-fc-sort* value on click event. The attribute *data-fc-sort should* be any valid field key in your collection. 133 | 134 | You will also have *fcSort*, a reactive template helper, to detect current sorting values. 135 | 136 | ### Sortable table headers 137 | 138 | ```html 139 | 140 | Some text 141 | {{#if fcSort.name.desc}}desc{{/if}} 142 | {{#if fcSort.name.asc}}asc{{/if}} 143 | 144 | ``` 145 | 146 | ### Clear Sorts 147 | ```html 148 | Clear sorting 149 | ``` 150 | 151 | ## Methods 152 | 153 | ### .sort.set(field, order, triggerUpdate) 154 | 155 | **field**: is a valid key in your collection. 156 | 157 | **order**: 'desc', 'asc' or null. 158 | 159 | **triggerUpdate**: boolean indicating if the subscriber must be updated after setting the new values. 160 | 161 | ```javascript 162 | PeopleFilter.sort.set(someCollectionField, 'desc', true); // this will set the field order to 'desc'. 163 | PeopleFilter.sort.set(someCollectionField, null, true); // this will loop over the sort stages described above (default: null, asc, desc) 164 | ``` 165 | 166 | This will change object sorting and will trigger a collection update (all at once) but you can also control the process yourself. 167 | 168 | ... 169 | PeopleFilter.sort.set(someCollectionField1, 'desc'); 170 | PeopleFilter.sort.set(someCollectionField2, 'desc'); 171 | PeopleFilter.sort.set(someCollectionField3, 'asc'); 172 | PeopleFilter.sort.set(someCollectionField4, 'desc'); 173 | PeopleFilter.sort.run(); 174 | ``` 175 | 176 | ### .sort.get() 177 | 178 | Will return the current sort status as an object. 179 | 180 | ```javascript 181 | var sortStatus = PeopleFilter.sort.get(); 182 | 183 | // sortStatus will be something like: 184 | // { 185 | // account_balance: {desc: true}, 186 | // created_at: {asc: true}, 187 | // name: {asc: true}, 188 | // } 189 | ``` 190 | 191 | ### .sort.run() 192 | 193 | Will take the current sorting status and trigger a query update to the subscriber to update results. 194 | 195 | ```javascript 196 | PeopleFilter.sort.run(); 197 | ``` 198 | 199 | ### .sort.clear() 200 | 201 | Will remove all sorting values. 202 | 203 | ```javascript 204 | PeopleFilter.sort.clear(); // Will remove values only. 205 | PeopleFilter.sort.clear(true); // Will remove values and trigger a query update. 206 | ``` 207 | 208 | # Paginating 209 | 210 | This package provides various pager methods and template helpers to easly manipulate your collection results. You can use all these features together or only some of them, based on your application needs. 211 | 212 | You can provide default collection sorting as: 213 | 214 | ```javascript 215 | PeopleFilter = new Meteor.FilterCollections(People, { 216 | //... 217 | pager: { 218 | options: [5, 10, 15, 25, 50], 219 | itemsPerPage: 5, 220 | currentPage: 1, 221 | showPages: 5, 222 | } 223 | //... 224 | }); 225 | ``` 226 | 227 | **options**: (optional, default is [10, 20, 30, 40, 50]) an array containing the allowed values to limit the collection results. 228 | 229 | **itemsPerPage**: (optional, default is 10) is the default limit applied to collection results. This will prevent us from loading all the collection documents at once and could be easly combined with pager.options values as described above setting the CSS class "fc-pager-options". 230 | 231 | **currentPage**: (optional, default is 1) will set the default page where the collection pager cursor must be at startup. 232 | 233 | **showPages**: (optional, default is 10) this argument represents the numbers of pages to be displayed on the classic pager. 234 | 235 | ## Templates helpers 236 | 237 | Then in your template you can do the following: 238 | 239 | ### Items per page 240 | 241 | Build a dropdown menu or custom links to let the user select the amount of results that should be displayed. 242 | 243 | ```html 244 | 245 | 250 | 251 | ``` 252 | 253 | **fcPager.options.value**: contains the row option value. 254 | 255 | **fcPager.options.status**: contains the row status (selected or an empty string). 256 | 257 | You can also add itemsPerPage behaviour with links or any DOM clickeable element if you specify the class "fc-pager-option" and a custom html attribute "data-fc-pager-page". 258 | 259 | ```html 260 | 261 | ten 262 | twenty 263 | thirty 264 | fourty 265 | fifty 266 | 267 | ``` 268 | 269 | ### Pager status. 270 | 271 | You can use package reactive datasources to notify the user where the current pager status. 272 | 273 | ```html 274 | 275 | 281 | 282 | ``` 283 | 284 | ### Classic Pager 285 | 286 | ```html 287 | 288 | 297 | 298 | ``` 299 | 300 | * **fc-pager-first** will move the currentPage value to the first page if possible. 301 | * **fc-pager-previous** will move the currentPage value to the previous page if possible. 302 | * **fc-pager-next** will move the currentPage value to the next page if present page if possible. 303 | * **fc-pager-first** will move the currentPage value to the last page if possible. 304 | * **fc-pager-page** will move the currentPage value to the specified number at "data-fc-pager-page" html attribute. 305 | 306 | **fcPager.pages.status**: active or an empty string. 307 | 308 | **fcPager.pages.page**: the current page number. 309 | 310 | ## Full pager example: 311 | 312 | ### Javascript 313 | 314 | ```javascript 315 | PeopleFilter = new Meteor.FilterCollections(People, { 316 | //... 317 | pager: { 318 | options: [5, 10, 15, 25, 50], 319 | itemsPerPage: 5, 320 | currentPage: 1, 321 | showPages: 5, 322 | } 323 | //... 324 | }); 325 | ``` 326 | 327 | ### Template 328 | 329 | ```html 330 | 331 | 336 | 337 | 338 | 344 | 345 | 346 | 355 | 356 | ``` 357 | 358 | ## Methods 359 | 360 | ### .pager.set(triggerUpdate) 361 | 362 | Will update pager template data based on current _pager status. 363 | 364 | **triggerUpdate**: boolean indicating if the subscriber must be updated after setting the new values. 365 | 366 | ```javascript 367 | PeopleFilter.pager.set(); // Will update template data only. 368 | PeopleFilter.pager.set(true); // Will update template data and collection results based on pager current status. 369 | ``` 370 | 371 | ### .pager.get() 372 | 373 | Will return the current _pager object. 374 | 375 | ```javascript 376 | var pagerStatus = PeopleFilter.pager.get(); 377 | ``` 378 | 379 | ### .pager.run() 380 | 381 | Will filter collection results based on the current pager status. 382 | 383 | ```javascript 384 | PeopleFilter.pager.run(); 385 | ``` 386 | 387 | ### .pager.moveTo(page) 388 | 389 | Will request collection publisher for update results for page. 390 | 391 | **page**: the page number to move the cursor. 392 | 393 | ```javascript 394 | PeopleFilter.pager.moveTo(4); 395 | ``` 396 | 397 | ### .pager.movePrevious() 398 | 399 | Will request collection publisher to update results for the previous page (if cursos is not in the first page already). 400 | 401 | ```javascript 402 | PeopleFilter.pager.movePrevious(); 403 | ``` 404 | 405 | ### .pager.moveFirst() 406 | 407 | Will request collection publisher to update results for the first page (if cursos is not in the first page already). 408 | 409 | ```javascript 410 | PeopleFilter.pager.moveFirst(); 411 | ``` 412 | 413 | ### .pager.moveNext() 414 | 415 | Will request collection publisher to update results for the next page (if cursos is not in the last page already). 416 | 417 | ```javascript 418 | PeopleFilter.pager.moveNext(); 419 | ``` 420 | 421 | ### .pager.moveLast() 422 | 423 | Will request collection publisher to update results for the last page (if cursos is not in the last page already). 424 | 425 | ```javascript 426 | PeopleFilter.pager.moveLast(); 427 | ``` 428 | 429 | ### .pager.setItemsPerPage(itemsNumber, triggerUpdate) 430 | 431 | Will request collection publisher to update results based on this limit. 432 | 433 | **itemsNumber**: the amount of items to be displayed. 434 | 435 | **triggerUpdate**: boolean indicating if the subscriber must be updated after setting the new values. 436 | 437 | ```javascript 438 | PeopleFilter.pager.setItemsPerPage(5); // Will update pager only. 439 | PeopleFilter.pager.setItemsPerPage(5, true); // Will update pager and collection results based on current status. 440 | ``` 441 | 442 | ### .pager.setCurrentPage(page, triggerUpdate) 443 | 444 | Will request collection publisher for update results for page. This differs from .pager.moveTo because there is no validation before moving the page cursor. 445 | 446 | **page**: the page number to move the cursor. 447 | 448 | **triggerUpdate**: boolean indicating if the subscriber must be updated after setting the new values. 449 | 450 | ```javascript 451 | PeopleFilter.pager.setCurrentPage(5); // Will update pager only. 452 | PeopleFilter.pager.setCurrentPage(5, true); // Will update pager and collection results based on current status. 453 | ``` 454 | 455 | # Filtering 456 | 457 | This package brings easy configurable filters to play with Meteor Collections's documents. 458 | To allow filtering, the package needs to know what fields are allowed to filter by. So: 459 | 460 | ```javascript 461 | PeopleFilter = new Meteor.FilterCollections(People, { 462 | //... 463 | filters: { 464 | name: { 465 | title: 'Complete name', 466 | operator: ['$regex', 'i'], 467 | condition: '$and', 468 | searchable: true 469 | }, 470 | account_balance: { 471 | title: 'Person Account Balance', 472 | condition: '$and', 473 | transform: function (value) { 474 | return parseFloat(value); 475 | }, 476 | sort: 'desc' 477 | }, 478 | type: { 479 | title: 'People Types' 480 | }, 481 | "contacts.name": { 482 | title: 'ContactName' 483 | } 484 | }, 485 | //... 486 | }); 487 | ``` 488 | 489 | Each filter setup have the following structure: 490 | 491 | key: { 492 | title: ... 493 | operator: ... 494 | condition: ... 495 | transform: ... 496 | searchable: ... 497 | sort: ... 498 | } 499 | 500 | **key**: the Collection's document field name. We can use also Mongo dot notation to work with nested fields as "contacts.name" for example. 501 | 502 | **title**: some human redable text to name the field for better display. 503 | 504 | **operator**: an array containig MongoDB operators for advance filtering. See [Mongo operators](http://docs.mongodb.org/manual/reference/operator/query/ "Mongo operators"). 505 | 506 | **condition**: also a MongoDB operator but to group filter criteria. (eg. {$and: [{field1: value},{field2: value}]}). 507 | 508 | **transform**: is a callback used to alter the filter value before performing a new subscription update (helpful if, for example, you have a price as number or float in your document and need to tranform your form value comming as string). 509 | 510 | **sort**: 'desc' or 'asc'. After the collection is filtered will clear current sort status and results will be sorted by this field. 511 | 512 | **searchable**: 'required' or 'optional'. If 'required', any search done will add a condition considering this field. If 'optional' you will have to activate this filter manually when searching. See Search below. 513 | 514 | *Note: the result of this configuration is a dynamic query passed to a subscriber and its publisher returning only filtered results based on recieved criteria.* 515 | 516 | ## Templates helpers 517 | 518 | ### Filter links 519 | 520 | ```html 521 | 522 | 523 | Show me my Customers 524 | 525 | 526 | Show me my Suppliers 527 | 528 | 533 | People who owe me money 534 | 535 | 540 | People to whom I owe money 541 | 542 | 543 | ``` 544 | 545 | Also you can use a custom list to build your filter links. 546 | 547 | ```javascript 548 | Template.peopleFilter.helpers({ 549 | //... 550 | categories: function(){ 551 | return [ 552 | { 553 | label: 'Category One', 554 | field: 'account_balance', 555 | value: 0, 556 | operator: '$gt', 557 | sort: 'desc' 558 | }, 559 | { 560 | label: 'Category Two', 561 | field: 'account_balance', 562 | value: 0, 563 | operator: '$lt' 564 | } 565 | ]; 566 | }, 567 | //... 568 | }); 569 | ``` 570 | 571 | ```html 572 | 573 | {{#each categories}} 574 | 579 | {{label}} 580 | 581 | {{/each}} 582 | 583 | ``` 584 | 585 | You can add the following attributes to any clickeable DOM element with the css class "fc-filter" attached. 586 | 587 | * **data-fc-filter-field**: required 588 | * **data-fc-filter-value**: required 589 | * **data-fc-filter-operator**: optional 590 | * **data-fc-filter-options**: optional 591 | * **data-fc-filter-sort**: optional 592 | 593 | Why do I have to specify filter setup twice? 594 | Well, first of all, you don't 'have' to. Attributes used in DOM will override the ones provided in configuration. The main idea for this package is to be flexible and let you use filters on HTML and JS at the same time or independently. 595 | 596 | For getting filter status from template (to set active classes for example), I've provided a template helper 'fcFilterObj' and 'fcPagerObj' to use object methods from your template. Example: 597 | 598 | ```html 599 | 600 | 601 | Show me my Customers 602 | 603 | 604 | Show me my Suppliers 605 | 606 | 607 | ``` 608 | 609 | or 610 | 611 | ```html 612 | 613 | {{#each categories}} 614 | 619 | {{label}} 620 | {{#if isActiveFilter field value operator}}This filter is active{{/if}} 621 | 622 | {{/each}} 623 | 624 | ``` 625 | 626 | ### Filter pills 627 | 628 | You have available **fcFilterActive** helper with a reactive datasource for display current filter status. 629 | 630 | ```html 631 |

Active Filters

632 | {{#each fcFilterActive}} 633 | 636 | {{/each}} 637 | 638 | ``` 639 | 640 | ## Methods 641 | 642 | ### .filter.get() 643 | 644 | Will return the allowed filter object with current status. 645 | 646 | ```javascript 647 | var filterDefinition = PeopleFilter.filter.get(); 648 | ``` 649 | 650 | ### .filter.set(key, filter, triggerUpdate) 651 | 652 | Will add or overwrite a filter. 653 | 654 | **key**: Collection field name. 655 | 656 | **filter**: an object to replace or add to the filter list (eg. {value: 1234, condition: '$or'}) 657 | 658 | **triggerUpdate**: boolean indicating if the subscriber must be updated after setting the new values. 659 | 660 | ```javascript 661 | PeopleFilter.filter.set('name', {value:'John', operator: ['$neq']}); // Will update the filter and perform a query update. 662 | PeopleFilter.filter.set('account_balance', {value:0, operator: ['$gt']}, false); // Will set the update but without performing a query update. 663 | ``` 664 | 665 | ### .filter.isActive(field, value, operator) 666 | 667 | Will return the allowed filter object with current status. 668 | 669 | **field**: the active filter key to check. 670 | 671 | **value**: the value to check for. 672 | 673 | **operator**: (optional) operator assigned (if not present, verification will be made only with field and value). 674 | 675 | ```javascript 676 | PeopleFilter.filter.isActive('name', 'John'); 677 | PeopleFilter.filter.isActive('account_balance', 0, '$lt'); 678 | PeopleFilter.filter.isActive('account_balance', 0, '$gt'); 679 | ``` 680 | 681 | ### .filter.run() 682 | 683 | Build the query with the current filter status and run a subscription update. 684 | 685 | ```javascript 686 | PeopleFilter.filter.run(); 687 | ``` 688 | 689 | ### .filter.clear(key, triggerUpdate) 690 | 691 | Clear active filters if no argument is recieved. 692 | 693 | **key**: Collection field name to be deleted from filters. 694 | 695 | **triggerUpdate**: boolean indicating if the subscriber must be updated after setting the new values. 696 | 697 | ```javascript 698 | PeopleFilter.filter.clear('name'); 699 | PeopleFilter.filter.clear(); 700 | ``` 701 | 702 | # Searching 703 | 704 | With the filter functionality we are able to set custom searches in no time. 705 | 706 | ```html 707 |
708 | 709 | {{#if fcFilterSearchable.criteria}}{{/if}} 710 | 711 |
712 | ``` 713 | 714 | When **fc-search-trigger** is clicked, the package will take the value **data-fc-search-trigger** and will look for a DOM element **data-fc-search-target** that match the value. 715 | Once there, will take all filters with the **searchable** value *('required' or 'optional')* and will perform a subscription update. 716 | 717 | ## Template helpers 718 | 719 | There is a **fcFilterSearchable** helper with **criteria** and **available** as childs. 720 | 721 | **criteria**: will maintain the value of the current search. 722 | 723 | **available**: is a list with all "searchable" fields. 724 | 725 | ### Toggle Search fields 726 | 727 | ```html 728 | {{#each available}} 729 | {{#if active}}Disable{{else}}Enable{{/if}} {{title}} filter 730 | {{/each}} 731 | ``` 732 | 733 | ## Methods 734 | 735 | ### .search.getFields(required) 736 | 737 | Returns all searchable fields. 738 | 739 | **required**: boolean indicating if the method should return all searchable fields 'required' and 'optional' (true) or only 'optional' (null or false) 740 | 741 | ```javascript 742 | var fields = PeopleFilter.search.getFields(true); 743 | ``` 744 | 745 | ### .search.setField(key) 746 | 747 | Will set the passed key as an active searchable filter. This will override default setup. 748 | 749 | **key**: a valid filter key. 750 | 751 | ```javascript 752 | PeopleFilter.search.setField('name'); 753 | PeopleFilter.search.setField('account_balance'); 754 | ``` 755 | 756 | ### .search.setCriteria(criteria, triggerUpdate) 757 | 758 | Will update the search value. 759 | 760 | **criteria**: value to be searched within searchable fields. 761 | 762 | **triggerUpdate**: boolean indicating if the subscriber must be updated after setting the new values (false by default). 763 | 764 | ```javascript 765 | PeopleFilter.search.setCriteria('Lorem Ipsum', true); //Will set the criteria and perform a subscription update 766 | PeopleFilter.search.setCriteria('Lorem Ipsum'); //Will only set the criteria 767 | ``` 768 | 769 | ### .search.getCriteria() 770 | 771 | Will return the current search value. 772 | 773 | ```javascript 774 | var search = PeopleFilter.search.getCriteria(); 775 | ``` 776 | 777 | ### .search.run() 778 | 779 | Build the query with the current search status and run a subscription update. 780 | 781 | ```javascript 782 | PeopleFilter.search.run(); 783 | ``` 784 | 785 | ### .search.clear(key, triggerUpdate) 786 | 787 | Clear active search if no argument is recieved. 788 | 789 | **key**: Collection field name to be deleted from search. 790 | 791 | **triggerUpdate**: boolean indicating if the subscriber must be updated after setting the new values. 792 | 793 | ```javascript 794 | PeopleFilter.search.clear('name'); // Will unset only the name field. 795 | PeopleFilter.search.clear(); // will unset all the active search and filters. 796 | ``` 797 | 798 | # Callbacks 799 | 800 | ## Client side 801 | 802 | You can intercept the query object before sent to the server and you can also intercept the subscription once is ready. 803 | 804 | ```javascript 805 | PeopleFilter = new Meteor.FilterCollections(People, { 806 | //... 807 | callbacks: { 808 | beforeSubscribe: function (query) { 809 | Session.set('loading', true); 810 | //return query (optional) 811 | }, 812 | afterSubscribe: function (subscription) { 813 | Session.set('loading', false); 814 | }, 815 | beforeResults: function(query){ 816 | query.selector._id = {$ne: Meteor.userId()}; 817 | return query; 818 | }, 819 | afterResults: function(cursor){ 820 | var alteredResults = cursor.fetch(); 821 | _.each(alteredResults, function(result, idx){ 822 | alteredResults[idx].name = alteredResults[idx].name.toUpperCase(); 823 | }); 824 | return alteredResults; 825 | }, 826 | templateCreated: function(template){}, 827 | templateRendered: function(template){}, 828 | templateDestroyed: function(template){} 829 | } 830 | //... 831 | }); 832 | ``` 833 | 834 | **beforeSubscribe**: you can use the passed query object for your own purpose or modify it before the request (this last one needs to return the query object). 835 | 836 | **afterSubscribe**: you can play with the subscription object and handle your own `ready()` statements. 837 | 838 | **templateCreated**: append behaviours to Template.name.created. 839 | 840 | **templateRendered**: append behaviours to Template.name.rendered. 841 | 842 | **templateDestroyed**: append behaviours to Template.name.destroyed. 843 | 844 | ## Server side 845 | 846 | ```javascript 847 | Meteor.FilterCollections.publish(People, { 848 | name: 'someName', 849 | callbacks: { 850 | allow: function(query, handler){ 851 | 852 | //... do some custom validation (like user permissions)... 853 | 854 | return false; 855 | }, 856 | beforePublish: function(query, handler){ 857 | 858 | if (Roles.userIsInRole(handler.userId, ['root'])) 859 | query.selector = _.omit(query.selector, 'deleted_at'); 860 | 861 | if (Roles.userIsInRole(handler.userId, ['administrator'])) 862 | query.selector = _.extend(query.selector, {deleted_at: { $exists: false }}); 863 | 864 | return query; 865 | }, 866 | afterPublish: function(cursor){ 867 | 868 | //... your cursor modifier code goes here ... 869 | 870 | return cursor; 871 | } 872 | } 873 | }); 874 | ``` 875 | 876 | **allow**: (true by default) allow callback will prevent the publisher to be executed. Should be helpful to do some custom validation from server-side. 877 | 878 | **beforePublish**: you can alter the query object before doing any Mongo stuff. 879 | 880 | **afterPublish**: you can play with the returned Collection Cursor object to alter the resulset. 881 | 882 | # Queries 883 | 884 | To perform custom queries and still get paging, filter and other package functionalities, there is a public reactive data source available to use with the following methods. 885 | 886 | ## .query.get() 887 | 888 | Will return the current query object. 889 | 890 | ```javascript 891 | PeopleFilter.query.get(); 892 | ``` 893 | 894 | ## .query.set(value) 895 | 896 | Will set a new query and update subscription results. 897 | 898 | **value**: an object with two properties **selector** and **options**. 899 | 900 | ```javascript 901 | var myQuery = { 902 | selector: { 903 | name: 'Lorem Ipsum' 904 | }, 905 | options: { 906 | limit: 300, 907 | skip: 0 908 | } 909 | }; 910 | PeopleFilter.query.set(myQuery); 911 | ``` 912 | 913 | # Contributors 914 | 915 | I've developed this module for a personal project when noticed that there was no tool at the moment that solve this common needs. 916 | 917 | Because of that I'll be glad to share ideas, consider suggestions and let contributors to help me maintain and improve the package to help the Meteor's Community. 918 | 919 | Let me know if you have any feedback (suggestions, bug, feature request, implementation, contribute, etc) and you can write me at [j@tooit.com](mailto:j@tooit.com "j@tooit.com"). 920 | 921 | Thanks for reading!, 922 | 923 | # Donate 924 | 925 | An easy and effective way to support the continued maintenance of this package and the development of new and useful packages is to donate through [Gittip](https://www.gittip.com/julianmontagna/ "Gittip"). or [Paypal](http://www.julianmontagna.com.ar/filter-collections.html "Paypal"). 926 | 927 | Gittip is a platform for sustainable crowd-funding. https://www.gittip.com/about/faq.html 928 | 929 | Help build an ecosystem of well maintained, quality Meteor packages by joining the Gittip Meteor Community. https://www.gittip.com/for/meteor/ 930 | 931 | # Hire 932 | 933 | Need support, debugging, or development for your project? You can [hire](http://www.linkedin.com/in/julianmontagna "hire") me to help out. 934 | --------------------------------------------------------------------------------