8 |
9 | {{> EasySearch.Input index=searchIndex charLimit=2 }}
10 |
11 | {{#EasySearch.IfInputEmpty index=searchIndex }}
12 | Search to see the magic!
13 | {{else}}
14 | {{#if resultsCount}}
15 | {{resultsCount}} results found.
16 | {{/if}}
17 | {{/EasySearch.IfInputEmpty}}
18 |
19 | {{#EasySearch.IfSearching index=searchIndex }}
20 | Searching...
21 | {{/EasySearch.IfSearching }}
22 |
23 |
24 | {{#EasySearch.Each index=searchIndex }}
25 | ...
26 | {{/EasySearch.Each}}
27 |
28 |
29 | {{#EasySearch.IfNoResults index=searchIndex }}
30 | No results found
31 | {{/EasySearch.IfNoResults }}
32 |
33 | {{> EasySearch.Pagination index=searchIndex maxPages=10 }}
34 |
35 | ```
36 |
37 | ## How to install
38 |
39 | ```sh
40 | cd /path/to/project
41 | meteor add easysearch:components
42 | ```
43 |
44 | NB: This package will use the `erasaur:meteor-lodash` package if it is already installed in your application, else it will fallback to the standard Meteor `underscore` package
45 |
--------------------------------------------------------------------------------
/packages/easysearch_components/component_methods.md:
--------------------------------------------------------------------------------
1 | # Component methods in EasySearch
2 |
3 | All the EasySearch components use the api that is [defined here](https://github.com/matteodem/meteor-easy-search/blob/master/packages/easysearch:components/lib/component-methods.js#L1). You can do
4 | things such as `search`, `loadMore` or `paginate` and render them by using `EasySearch.Each` (and other components).
5 |
6 | ```js
7 | import { peopleIndex } from './search/people-index'
8 | // instanceof EasySearch Index
9 |
10 | const methods = peopleIndex.getComponentMethods(/* optional name */)
11 |
12 | methods.search('peter')
13 | ```
14 |
15 | Consider having a look at the [source code](https://github.com/matteodem/meteor-easy-search/blob/master/packages/easysearch:components/lib/component-methods.js#L1) for all the methods.
16 | Don't forget to pass in a name if you have defined one in your blaze components.
17 |
--------------------------------------------------------------------------------
/packages/easysearch_components/lib/base.js:
--------------------------------------------------------------------------------
1 | /**
2 | * The BaseComponent holds the base logic for EasySearch Components.
3 | *
4 | * @type {BaseComponent}
5 | */
6 | BaseComponent = class BaseComponent extends BlazeComponent {
7 | /**
8 | * Return the name of the component.
9 | *
10 | * @returns {String}
11 | */
12 | get name() {
13 | return this.getData().name;
14 | }
15 |
16 | /**
17 | * Return an array of properties that are reserved to the base component.
18 | *
19 | * @returns {String[]}
20 | */
21 | static get reserveredProperties() {
22 | return ['index', 'indexes', 'name', 'attributes'];
23 | }
24 |
25 | /**
26 | * Setup component on created.
27 | */
28 | onCreated() {
29 | this.autorun(() => this.initializeBase());
30 | }
31 |
32 | initializeBase() {
33 | let index = this.getData().index,
34 | indexes = [index];
35 |
36 | if (!index) {
37 | indexes = this.getData().indexes;
38 | }
39 |
40 | if (_.isEmpty(indexes)) {
41 | throw new Meteor.Error('no-index', 'Please provide an index for your component');
42 | }
43 |
44 | if (indexes.filter((index) => index instanceof EasySearch.Index).length !== indexes.length) {
45 | throw new Meteor.Error(
46 | 'invalid-configuration',
47 | `Did not receive an index or an array of indexes: "${indexes.toString()}"`
48 | );
49 | }
50 |
51 | this.indexes = indexes;
52 | this.options = _.defaults({}, _.omit(this.getData(), ...BaseComponent.reserveredProperties), this.defaultOptions);
53 |
54 | check(this.name, Match.Optional(String));
55 | check(this.options, Object);
56 |
57 | this.eachIndex(function (index, name) {
58 | if (!index.getComponentDict(name)) {
59 | index.registerComponent(name);
60 | }
61 | });
62 | }
63 |
64 | /**
65 | * Return the default options.
66 | *
67 | * @returns {Object}
68 | */
69 | get defaultOptions () {
70 | return {};
71 | }
72 |
73 | /**
74 | * @param {String} searchStr
75 | *
76 | * @returns {Boolean}
77 | */
78 | shouldShowDocuments(searchStr) {
79 | return !this.getData().noDocumentsOnEmpty || 0 < searchStr.length;
80 | }
81 |
82 | /**
83 | * Search the component.
84 | *
85 | * @param {String} searchString String to search for
86 | */
87 | search(searchString) {
88 | check(searchString, String);
89 |
90 | const showDocuments = this.shouldShowDocuments(searchString);
91 |
92 | this.eachIndex(function (index, name) {
93 | index.getComponentDict(name).set('showDocuments', showDocuments);
94 |
95 | if (showDocuments) {
96 | index.getComponentMethods(name).search(searchString);
97 | }
98 | });
99 | }
100 |
101 | /**
102 | * Return the data.
103 | *
104 | * @returns {Object}
105 | */
106 | getData() {
107 | return (this.data() || {});
108 | }
109 |
110 | /**
111 | * Return the dictionaries.
112 | *
113 | * @returns {Object}
114 | */
115 | get dicts() {
116 | return this.eachIndex((index, name) => {
117 | return index.getComponentDict(name);
118 | }, 'map');
119 | }
120 |
121 | /**
122 | * Loop through each index and apply a function
123 | *
124 | * @param {Function} func Function to run
125 | * @param {String} method Lodash method name
126 | *
127 | * @return mixed
128 | */
129 | eachIndex(func, method = 'each') {
130 | let componentScope = this,
131 | logic = this.getData().logic;
132 |
133 | if (!_.isEmpty(logic)) {
134 | method = 'OR' === logic ? 'some' : 'every';
135 | }
136 |
137 | return _[method](this.indexes, function (index) {
138 | return func.apply(this, [index, componentScope.name]);
139 | });
140 | }
141 | };
142 |
143 | EasySearch.BaseComponent = BaseComponent;
144 |
--------------------------------------------------------------------------------
/packages/easysearch_components/lib/component-methods.js:
--------------------------------------------------------------------------------
1 | EasySearch._getComponentMethods = function (dict, index) {
2 | return {
3 | /**
4 | * Search a component for the given search string.
5 | *
6 | * @param {Object|String} searchDefinition Search definition
7 | */
8 | search: (searchDefinition) => {
9 | dict.set('searchOptions', {
10 | props: (dict.get('searchOptions') || {}).props
11 | });
12 |
13 | dict.set('searchDefinition', searchDefinition);
14 | dict.set('stopPublication', true);
15 | },
16 | /**
17 | * Return the EasySearch.Cursor for the current search.
18 | *
19 | * @returns {Cursor}
20 | */
21 | getCursor: () => {
22 | const searchDefinition = dict.get('searchDefinition') || '',
23 | options = dict.get('searchOptions') || {},
24 | showDocuments = dict.get('showDocuments');
25 |
26 | check(options, Match.Optional(Object));
27 |
28 | if (false === showDocuments) {
29 | dict.set('count', 0);
30 | dict.set('searching', false);
31 | dict.set('limit', 0);
32 | dict.set('skip', 0);
33 | dict.set('currentCount', 0);
34 | dict.set('stopPublication', false);
35 |
36 | return EasySearch.Cursor.emptyCursor;
37 | }
38 |
39 | const cursor = index.search(searchDefinition, options),
40 | searchOptions = index._getSearchOptions(options);
41 |
42 | dict.set('count', cursor.count());
43 | dict.set('searching', !cursor.isReady());
44 | dict.set('limit', searchOptions.limit);
45 | dict.set('skip', searchOptions.skip);
46 | dict.set('currentCount', cursor.mongoCursor.count());
47 | dict.set('stopPublication', false);
48 |
49 | return cursor;
50 | },
51 | /**
52 | * Return true if the current search string is empty.
53 | *
54 | * @returns {boolean}
55 | */
56 | searchIsEmpty: () => {
57 | let searchDefinition = dict.get('searchDefinition');
58 |
59 | return !searchDefinition || (_.isString(searchDefinition) && 0 === searchDefinition.trim().length);
60 | },
61 | /**
62 | * Return true if the component has no results.
63 | *
64 | * @returns {boolean}
65 | */
66 | hasNoResults: () => {
67 | let count = dict.get('count'),
68 | showDocuments = dict.get('showDocuments');
69 |
70 | return false !== showDocuments
71 | && !dict.get('searching')
72 | && (!_.isNumber(count) || 0 === count);
73 | },
74 | /**
75 | * Return true if the component is being searched.
76 | *
77 | * @returns {boolean}
78 | */
79 | isSearching: () => {
80 | return !!dict.get('searching');
81 | },
82 | /**
83 | * Return true if the component has more documents than displayed right now.
84 | *
85 | * @returns {boolean}
86 | */
87 | hasMoreDocuments: () => {
88 | return dict.get('currentCount') < dict.get('count');
89 | },
90 | /**
91 | * Load more documents for the component.
92 | *
93 | * @param {Number} count Count of docs
94 | */
95 | loadMore: (count) => {
96 | check(count, Number);
97 |
98 | let currentCount = dict.get('currentCount'),
99 | options = dict.get('searchOptions') || {};
100 |
101 | options.limit = currentCount + count;
102 | dict.set('searchOptions', options);
103 | },
104 | /**
105 | * Paginate through documents for the given page.
106 | *
107 | * @param {Number} page Page number
108 | */
109 | paginate: (page) => {
110 | check(page, Number);
111 |
112 | let options = dict.get('searchOptions') || {},
113 | limit = dict.get('limit');
114 |
115 | options.skip = limit * (page - 1);
116 | dict.set('currentPage', page);
117 | dict.set('searchOptions', options);
118 | dict.set('stopPublication', true);
119 | },
120 | /**
121 | * Add custom properties for search.
122 | */
123 | addProps(...args) {
124 | let options = dict.get('searchOptions') || {};
125 |
126 | options.props = options.props || {};
127 |
128 | if (_.isObject(args[0])) {
129 | options.props = _.extend(options.props, args[0]);
130 | } else if (_.isString(args[0])) {
131 | options.props[args[0]] = args[1];
132 | }
133 |
134 | dict.set('searchOptions', options);
135 | this.paginate(1);
136 | },
137 | /**
138 | * Remove custom properties for search.
139 | */
140 | removeProps(...args) {
141 | let options = dict.get('searchOptions') || {};
142 |
143 | if (!_.isEmpty(args)) {
144 | options.props = _.omit(options.props, args) || {};
145 | } else {
146 | options.props = {};
147 | }
148 |
149 | dict.set('searchOptions', options);
150 | this.paginate(1);
151 | },
152 | /**
153 | * Reset the search.
154 | */
155 | reset() {
156 | this.search('');
157 | this.paginate(1);
158 | dict.set('searchOptions', {});
159 | },
160 | };
161 | };
162 |
--------------------------------------------------------------------------------
/packages/easysearch_components/lib/core.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Extend EasySearch.Index with component functionality.
3 | *
4 | * @type {Index}
5 | */
6 | EasySearch.Index = class Index extends EasySearch.Index {
7 | /**
8 | * Constructor.
9 | */
10 | constructor() {
11 | super(...arguments);
12 | this.components = {};
13 | }
14 |
15 | /**
16 | * Return static default name for components.
17 | *
18 | * @returns {String}
19 | */
20 | static get COMPONENT_DEFAULT_NAME() {
21 | return'__default';
22 | }
23 |
24 | /**
25 | * Register a component on the index.
26 | *
27 | * @param {String} componentName Optional name of the component
28 | */
29 | registerComponent(componentName = EasySearch.Index.COMPONENT_DEFAULT_NAME) {
30 | this.components[componentName] = new ReactiveDict(
31 | `easySearchComponent_${this.config.name}_${componentName}_${Random.id()}`
32 | );
33 | }
34 |
35 | /**
36 | * Get the reactive dictionary for a component.
37 | *
38 | * @param {String} componentName Optional name of the component
39 | */
40 | getComponentDict(componentName = EasySearch.Index.COMPONENT_DEFAULT_NAME) {
41 | return this.components[componentName];
42 | }
43 |
44 | /**
45 | * Get component methods that are useful for implementing search behaviour.
46 | *
47 | * @param componentName
48 | */
49 | getComponentMethods(componentName = EasySearch.Index.COMPONENT_DEFAULT_NAME) {
50 | let dict = this.getComponentDict(componentName);
51 |
52 | if (!dict) {
53 | throw new Meteor.Error('no-component', `Component with name '${componentName}' not found`);
54 | }
55 |
56 | return EasySearch._getComponentMethods(dict, this);
57 | }
58 | };
59 |
60 | /**
61 | * Return true if the current page is valid.
62 | *
63 | * @param {Number} totalPagesLength Count of all pages available
64 | * @param {Number} currentPage Current page to check
65 | *
66 | * @returns {boolean}
67 | */
68 | function isValidPage(totalPagesLength, currentPage) {
69 | return currentPage <= totalPagesLength && currentPage > 0;
70 | }
71 |
72 | /**
73 | * Helper method to get the pages for pagination as an array.
74 | *
75 | * @param totalCount Total count of results
76 | * @param pageCount Count of results per page
77 | * @param currentPage Current page
78 | * @param prevAndNext True if Next and Previous buttons should appear
79 | * @param maxPages Maximum count of pages to show
80 | *
81 | * @private
82 | *
83 | * @returns {Array}
84 | */
85 | EasySearch._getPagesForPagination = function ({totalCount, pageCount, currentPage, prevAndNext, maxPages}) {
86 | let pages = _.range(1, Math.ceil(totalCount / pageCount) + 1),
87 | pagesLength = pages.length;
88 |
89 | if (!isValidPage(pagesLength, currentPage)) {
90 | throw new Meteor.Error('invalid-page', 'Current page is not in valid range');
91 | }
92 |
93 | if (maxPages) {
94 | let startSlice = (currentPage > (maxPages / 2) ? (currentPage - 1) - Math.floor(maxPages / 2) : 0),
95 | endSlice = startSlice + maxPages;
96 |
97 | if (endSlice > pagesLength) {
98 | pages = pages.slice(-maxPages);
99 | } else {
100 | pages = pages.slice(startSlice, startSlice + maxPages);
101 | }
102 | }
103 |
104 | let pageData = _.map(pages, function (page) {
105 | let isCurrentPage = page === currentPage;
106 | return { page, content: page.toString(), current: isCurrentPage, disabled: isCurrentPage };
107 | });
108 |
109 | if (prevAndNext) {
110 | // Previous
111 | let prevPage = isValidPage(pagesLength, currentPage - 1) ? currentPage - 1 : null;
112 | pageData.unshift({ page: prevPage, content: 'Prev', current: false, disabled: 1 === currentPage });
113 | // Next
114 | let nextPage = isValidPage(pagesLength, currentPage + 1) ? currentPage + 1 : null;
115 | pageData.push(
116 | { page: nextPage, content: 'Next', current: false, disabled: null == nextPage || pagesLength + 1 === currentPage }
117 | );
118 | }
119 |
120 | return pageData;
121 | };
122 |
--------------------------------------------------------------------------------
/packages/easysearch_components/lib/each/each.html:
--------------------------------------------------------------------------------
1 |