├── .gitignore ├── .versions ├── MeteorGriddle.jsx ├── README.md └── package.js /.gitignore: -------------------------------------------------------------------------------- 1 | .npm -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | allow-deny@1.0.4 2 | babel-compiler@6.6.4 3 | babel-runtime@0.1.8 4 | base64@1.0.8 5 | binary-heap@1.0.8 6 | blaze@2.1.7 7 | blaze-tools@1.0.8 8 | boilerplate-generator@1.0.8 9 | callback-hook@1.0.8 10 | check@1.2.1 11 | ddp@1.2.5 12 | ddp-client@1.2.7 13 | ddp-common@1.2.5 14 | ddp-server@1.2.6 15 | deps@1.0.12 16 | diff-sequence@1.0.5 17 | ecmascript@0.4.3 18 | ecmascript-runtime@0.2.10 19 | ejson@1.0.11 20 | geojson-utils@1.0.8 21 | html-tools@1.0.9 22 | htmljs@1.0.9 23 | id-map@1.0.7 24 | jquery@1.11.8 25 | jsx@0.2.4 26 | logging@1.0.12 27 | meteor@1.1.14 28 | minimongo@1.0.16 29 | modules@0.6.1 30 | modules-runtime@0.6.3 31 | mongo@1.1.7 32 | mongo-id@1.0.4 33 | npm-mongo@1.4.43 34 | observe-sequence@1.0.11 35 | ordered-dict@1.0.7 36 | promise@0.6.7 37 | random@1.0.9 38 | react-meteor-data@0.2.9 39 | reactive-var@1.0.9 40 | retry@1.0.7 41 | routepolicy@1.0.10 42 | spacebars@1.0.11 43 | spacebars-compiler@1.0.11 44 | tmeasday:check-npm-versions@0.2.0 45 | tmeasday:publish-counts@0.7.3 46 | tracker@1.0.13 47 | ui@1.0.11 48 | underscore@1.0.8 49 | utilities:meteor-griddle@1.2.1 50 | webapp@1.2.8 51 | webapp-hashing@1.0.9 52 | -------------------------------------------------------------------------------- /MeteorGriddle.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { checkNpmVersions } from 'meteor/tmeasday:check-npm-versions'; 3 | import { _ } from 'meteor/underscore'; 4 | import Griddle from 'griddle-react'; 5 | 6 | checkNpmVersions({ 7 | 'griddle-react': '0.5.x', 8 | 'react-addons-pure-render-mixin': '15.x', 9 | }, 'utilities:meteor-griddle'); 10 | 11 | MeteorGriddle = React.createClass({ 12 | 13 | propTypes: { 14 | publication: React.PropTypes.string, // the publication that will provide the data 15 | collection: React.PropTypes.object, // the collection to display 16 | matchingResultsCount: React.PropTypes.string, // the name of the matching results counter 17 | filteredFields: React.PropTypes.array, // an array of fields to search through when filtering 18 | subsManager: React.PropTypes.object, 19 | // plus regular Griddle props 20 | }, 21 | 22 | mixins: [ReactMeteorData], 23 | 24 | getDefaultProps() { 25 | return { 26 | useExternal: false, 27 | externalFilterDebounceWait: 300, 28 | externalResultsPerPage: 10, 29 | }; 30 | }, 31 | 32 | getInitialState() { 33 | 34 | return { 35 | currentPage: 0, 36 | maxPages: 0, 37 | externalResultsPerPage: this.props.externalResultsPerPage, 38 | externalSortColumn: this.props.externalSortColumn, 39 | externalSortAscending: this.props.externalSortAscending, 40 | query: {}, 41 | }; 42 | 43 | }, 44 | 45 | componentWillMount() { 46 | this.applyQuery = _.debounce((query) => { 47 | this.setState({ query }); 48 | }, this.props.externalFilterDebounceWait); 49 | }, 50 | 51 | getMeteorData() { 52 | 53 | // Get a count of the number of items matching the current filter. 54 | // If no filter is set it will return the total number of items in the 55 | // collection. 56 | var matchingResults = Counts.get(this.props.matchingResultsCount); 57 | 58 | const options = {}; 59 | let skip; 60 | if (this.props.useExternal) { 61 | options.limit = this.state.externalResultsPerPage; 62 | if (!_.isEmpty(this.state.query) && !!matchingResults) { 63 | // if necessary, limit the cursor to number of matching results to avoid 64 | // displaying results from other publications 65 | options.limit = _.min([options.limit, matchingResults]); 66 | } 67 | options.sort = { 68 | [this.state.externalSortColumn]: 69 | (this.state.externalSortAscending ? 1 : -1) 70 | }; 71 | skip = this.state.currentPage * this.state.externalResultsPerPage; 72 | } 73 | 74 | let pubHandle; 75 | 76 | if (this.props.subsManager) { 77 | pubHandle = this.props.subsManager.subscribe( 78 | this.props.publication, 79 | this.state.query, 80 | _.extend({skip: skip}, options) 81 | ); 82 | } else { 83 | pubHandle = Meteor.subscribe( 84 | this.props.publication, 85 | this.state.query, 86 | _.extend({skip: skip}, options) 87 | ); 88 | } 89 | 90 | const results = 91 | this.props.collection.find(this.state.query, options).fetch(); 92 | 93 | return { 94 | loading: !pubHandle.ready(), 95 | results: results, 96 | matchingResults: matchingResults 97 | } 98 | }, 99 | 100 | resetQuery() { 101 | this.setState({ 102 | query: {}, 103 | }); 104 | }, 105 | 106 | //what page is currently viewed 107 | setPage(index) { 108 | this.setState({currentPage: index}); 109 | }, 110 | 111 | //this changes whether data is sorted in ascending or descending order 112 | changeSort(sort, sortAscending) { 113 | this.setState({externalSortColumn: sort, externalSortAscending: sortAscending}); 114 | }, 115 | 116 | setFilter(filter) { 117 | if (filter) { 118 | const filteredFields = this.props.filteredFields || this.props.columns; 119 | const orArray = filteredFields.map((field) => { 120 | const filterItem = {}; 121 | filterItem[field] = {$regex: filter, $options: 'i'}; 122 | return filterItem; 123 | }); 124 | this.applyQuery({ $or: orArray }); 125 | } else { 126 | this.resetQuery(); 127 | } 128 | }, 129 | 130 | //this method handles determining the page size 131 | setPageSize(size) { 132 | this.setState({ externalResultsPerPage: size }); 133 | }, 134 | 135 | render() { 136 | 137 | // figure out how many pages we have based on the number of total results 138 | // matching the cursor 139 | var maxPages = 140 | Math.ceil(this.data.matchingResults/this.state.externalResultsPerPage); 141 | 142 | // The Griddle externalIsLoading property is managed internally to line 143 | // up with the subscription ready state, so we're removing this property 144 | // if it's passed in. 145 | const allProps = this.props; 146 | delete allProps.externalIsLoading; 147 | 148 | return ( 149 | 165 | ) 166 | 167 | } 168 | 169 | }); 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Meteor-Griddle 2 | 3 | A smart Meteor wrapper for the [Griddle](http://griddlegriddle.github.io/Griddle/) React component 4 | 5 | ## Installation 6 | 7 | - `meteor add utilities:meteor-griddle` 8 | - `npm install --save griddle-react` 9 | 10 | ## Usage 11 | 12 | ### React Component 13 | 14 | ``` 15 | import { MeteorGriddle } from 'meteor/utilities:meteor-griddle'; 16 | ``` 17 | 18 | The `` React component takes the same options as ``, plus a couple extra ones: 19 | 20 | #### Options 21 | 22 | - `publication`: the publication that will provide the data 23 | - `collection`: the collection to display 24 | - `matchingResultsCount`: the name of the matching results counter 25 | - `filteredFields`: an array of fields to search through when filtering 26 | - `subsManager`: An optional [meteorhacks:subs-manager](https://atmospherejs.com/meteorhacks/subs-manager) instance 27 | - `externalFilterDebounceWait`: When filtering data loaded from an external source, this time (in milliseconds) will be used to debounce the filter (to prevent results from refreshing on each keystroke). Default time is set to 300 ms. This property will only be used if the `useExternal` component property is set to `true`. For now the filter debounce option is limited to external data sources only. 28 | 29 | #### Example 30 | 31 | ```jsx 32 | 39 | ``` 40 | 41 | You'll usually want to pass along some of Griddle's [own options](http://griddlegriddle.github.io/Griddle/properties.html), too. 42 | 43 | #### Loading Message Customization 44 | 45 | If you're interested in displaying a custom table loading indicator/message, use the Griddle supported `externalLoadingComponent` property (which accepts a React Component): 46 | 47 | ```jsx 48 | 54 | ``` 55 | **Note:** Griddle uses the `externalIsLoading` (boolean) property to decide if the loading component should be shown or not. MeteorGriddle takes care of setting this property internally based on the subscription ready state. You do not need to pass this property in (and if you do it will be ignored). 56 | 57 | #### Filtering 58 | 59 | To show and use the Griddle filtering option, you must pass in either a `filteredFields` or `columns` property, as well as a `showFilter` property that's set to `true`. 60 | 61 | ```jsx 62 | 69 | ``` 70 | 71 | #### External Options 72 | 73 | To use any of Griddle's `external*` properties, you must pass in `useExternal` (set to `true`). `useExternal` is set to `false` by default. 74 | 75 | ```jsx 76 | // Will ignore `externalResultsPerPage` 77 | 83 | 84 | // Will use `externalResultsPerPage` 85 | 92 | ``` 93 | 94 | ### Publication 95 | 96 | To use Griddle, you need to define a publication in your own codebase. That publication takes two `query` and `options` arguments from the client. 97 | 98 | #### Example 99 | 100 | ```js 101 | Meteor.publish('adminUsers', function (query, options) { 102 | 103 | if(Users.isAdminById(this.userId)){ 104 | 105 | var users = Meteor.users.find(query, options); 106 | 107 | // can't reuse "users" cursor 108 | Counts.publish(this, 'matching-users', Meteor.users.find(query, options)); 109 | 110 | return users; 111 | } 112 | }); 113 | ``` 114 | 115 | #### Notes 116 | 117 | - The publication should publish a count of matching results using the [Publish Counts](https://github.com/percolatestudio/publish-counts) package. 118 | - Note that [an issue with the Publish Counts package](https://github.com/percolatestudio/publish-counts/issues/58) prevents you from reusing the same cursor. 119 | - You're trusted to make your own security checks on the `query` and `options` arguments. 120 | 121 | #### SubsManager 122 | 123 | The [meteorhacks:subs-manager](https://atmospherejs.com/meteorhacks/subs-manager) package can be used with `MeteorGriddle` to help cache subscriptions. Simply create a new SubsManager instance then pass it in via the `subsManager` property. For example: 124 | 125 | ```jsx 126 | const subsManager = new SubsManager(); 127 | const ProductList = () => ( 128 |
129 | 134 |
135 | ); 136 | ``` 137 | 138 | ## History 139 | 140 | ### 1.2.0 141 | 142 | - Clarified docs mentioning must have filtered fields or columns defined to use filter. 143 | - Adjusted so `useExternal` must be set to use `external*` properties. Set to `false` by default. 144 | - Added `externalFilterDebounceWait` property for controlling external data source filter debouncing. 145 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'utilities:meteor-griddle', 3 | summary: 'A smart Meteor wrapper for the Griddle React component', 4 | version: '1.2.2', 5 | git: 'https://github.com/meteor-utilities/meteor-griddle.git' 6 | }); 7 | 8 | Package.onUse(function(api) { 9 | 10 | api.versionsFrom('METEOR@1.3'); 11 | 12 | api.use([ 13 | 'tmeasday:publish-counts@0.7.3', 14 | 'react-meteor-data@0.2.9', 15 | 'jsx@0.2.4', 16 | 'tmeasday:check-npm-versions@0.2.0', 17 | 'underscore' 18 | ]); 19 | 20 | api.addFiles([ 21 | 'MeteorGriddle.jsx' 22 | ], 'client'); 23 | 24 | api.export([ 25 | 'MeteorGriddle' 26 | ]); 27 | 28 | }); 29 | --------------------------------------------------------------------------------