├── .versions
├── CHANGELOG.md
├── LICENSE.txt
├── README.md
├── examples
└── leaderboard
│ ├── .meteor
│ ├── .finished-upgraders
│ ├── .gitignore
│ ├── .id
│ ├── packages
│ ├── platforms
│ ├── release
│ └── versions
│ ├── README.md
│ ├── leaderboard.css
│ ├── leaderboard.html
│ └── leaderboard.jsx
├── package.js
└── src
├── DDPMixin.js
└── ReactiveMixin.js
/.versions:
--------------------------------------------------------------------------------
1 | grove:react@0.3.0
2 | meteor@1.1.6
3 | reactive-var@1.0.5
4 | tracker@1.0.7
5 | underscore@1.0.3
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ### v0.3.0
2 | * No longer includes React vendor files
3 |
4 | ### v0.2.0
5 | * Subscription setup moved to componentWillMount. Now have access to this.state
6 | within the `subscriptions` method.
7 | * React addons back in
8 |
9 | ### v0.1.4
10 | Updating React to v0.13.3
11 |
12 | ### v0.1.3
13 | Add true state for reactivevar when no subs
14 |
15 | ### v0.1.2
16 | * Exporting React on the server
17 |
18 | ### v0.1.1
19 | * Leaderboard example added
20 |
21 | ### v0.1.0
22 | Initial release
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Grove Labs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #DEPRECATED
2 | This package has been deprecated in favor of the official Meteor React package, [`react-meteor-data`](https://react-in-meteor.readthedocs.org/en/latest/meteor-data/).
3 |
4 | # Meteor & React
5 | This a [Meteor](https://meteor.com) package that includes 2 React mixins that enable binding reactive data sources and DDP subscriptions to a React Component.
6 |
7 | *If you're looking for a JSX compiler see [`grigio:babel`](https://github.com/grigio/meteor-babel)*
8 |
9 | ## Table of Contents
10 | 1. [Usage](#usage)
11 | 2. [ReactiveMixin](#reactivemixin)
12 | 3. [DDPMixin](#ddpmixin)
13 | 2. [Loading React](#loading-react)
14 | 2. [How it works](#how-it-works)
15 | 3. [Future Fork](#future-work)
16 |
17 | ## Usage
18 | ### Installation
19 |
20 | ```
21 | meteor add grove:react
22 | ```
23 |
24 | ### ReactiveMixin
25 | This mixin provides a way of binding reactive data sources to a React
26 | component. Components that use this mixin should implement a method
27 | named `getReactiveState` and return an object from that method, much
28 | like the standard `getInitialState`. Within the `getReactiveState`
29 | function you must make sure to call on [reactive data sources](http://docs.meteor.com/#/full/reactivity) or else the method won't rerun.
30 |
31 |
32 |
33 | #### Example
34 | ```js
35 | Bookface = React.createClass({
36 | mixins: [ ReactiveMixin ],
37 |
38 | getReactiveState: function() {
39 | return {
40 | friends: Friends.find().fetch(),
41 | loggedIn: !!Meteor.user()
42 | }
43 | },
44 |
45 | render: function() {
46 | if (this.state.loggedIn) {
47 | if (this.state.friends.length > 0) {
48 | return
You've got friends!
49 | }
50 | return
Forever alone...
;
51 | }
52 | return
Please log in
53 | }
54 | });
55 | ```
56 |
57 | ### DDPMixin
58 | This mixin provides a way of binding DDP subscriptions to a React component. Components that use this mixin should implement a method named `subscriptions` and return either a single [subscription handle](http://docs.meteor.com/#/full/meteor_subscribe) or an array of subscription handles. You can then call `subsReady()` within `getReactiveState` to reactively wait on them, or check `this.state.subsReady` from within the render function to see if they're ready. (You can also call `this.subsReady()` from within `render` but it follows React convention more closely to use the component's state).
59 |
60 | Make sure to include the DDPMixin _before_ ReactiveMixin for the component. As stated in the [Reusable Component docs](https://facebook.github.io/react/docs/reusable-components.html#mixins), _"methods defined on mixins run in the order mixins were listed, followed by... the component."_ DDPMixin must run first and define the `subsReady` function before it can be called within `getReactiveState`.
61 |
62 | An interesting use case might be to have a component that makes its own DDP connection and uses that instead of the default Meteor connection. `Meteor.subscribe` is just a [bound wrapper around a DDP connection](https://github.com/meteor/meteor/blob/devel/packages/ddp/client_convenience.js#L45-L56)
63 |
64 | A `SubsManager` object from [`meteorhacks:subs-manager`](https://github.com/meteorhacks/subs-manager) could also be used instead of `Meteor.subscribe`.
65 |
66 | #### Example
67 |
68 | ```js
69 | Post = React.createClass({
70 | mixins: [ DDPMixin, ReactiveMixin ],
71 |
72 | getInitialState: function () {
73 | return {
74 | post: null
75 | };
76 | },
77 |
78 | getReactiveState: function() {
79 | if ( this.subsReady() ) {
80 | var p = Posts.findOne( Session.get('currentPostId') );
81 | return { post: p };
82 | }
83 | },
84 |
85 | subscriptions: function() {
86 | return Meteor.subscribe('singlePost', Session.get('currentPost') );
87 | },
88 |
89 | // or if you want to wait on multiple subscriptions
90 | subscriptions: function() {
91 | return [
92 | Meteor.subscribe('singlePost', Session.get('currentPostId') ),
93 | Meteor.subscribe('postComments', Session.get('currentPostId') )
94 | ];
95 | },
96 |
97 | render: function() {
98 | if ( this.state.post ) { // or this.state.subsReady, but if the publication
99 | return ( // was empty then this.state.post is undefined
100 |
101 |
{this.state.post}
102 |
103 | );
104 | }
105 | return
Loading...
;
106 | }
107 | });
108 | ```
109 | For more modularity you could use the params from your Router instead of `Session.get`
110 |
111 | ## Loading React
112 | It's recommended to use [`cosmos:browserify`](https://github.com/elidoran/cosmos-browserify/) to get the React library itself. To do so correctly, you'll want to create a local package, `require` what you want, and then explicitly export them. You want to use a local package so that it gets loaded in before your application. For example, if you want to use React with addons and React Router:
113 |
114 | ```js
115 | // packages/client-deps/package.js
116 | Package.describe({
117 | name: 'client-deps',
118 | });
119 |
120 | Npm.depends({
121 | "react" : "0.13.3",
122 | "react-router" : "0.13.3"
123 | });
124 |
125 | Package.onUse(function(api) {
126 | api.use(['cosmos:browserify@0.2.0']);
127 | api.addFiles(['browserify.js']);
128 | api.export(['React', 'ReactRouter']);
129 | });
130 | ```
131 |
132 | ```js
133 | // packages/client-deps/browserify.js
134 | React = require('react/addons');
135 | ReactRouter = require('react-router');
136 | ```
137 |
138 | `React` and `ReactRouter` will then be exposed at the global scope to both the client and server.
139 |
140 | **A note on using the production version of React**: The difference between the development and production versions of React is more than just minification. There are warnings that are removed and optimizations made. To remove these warnings, make sure that your `NODE_ENV` environment variable is set to `"production"` when building your app. The [envify](https://www.npmjs.com/package/envify) transform present in cosmos:browserify will replace `process.env.NODE_ENV` with `"production"` throughout the library, and then when Meteor runs [UglifyJS](https://github.com/mishoo/UglifyJS2) it'll eliminate the now-dead code.
141 |
142 | ## How it works
143 | ### Reactive Subscriptions
144 | All of the subscription handles that are returned from `subscriptions` have `.ready()` called on them, which is a reactive data source. Every time a new subscription becomes ready it will check them all again. Once they are all ready, a `ReactiveVar` is set to true. The component method `this.subsReady()` is actually a bound `get` call on a ReactiveVar.
145 |
146 | ```js
147 | var subsReady = new ReactiveVar(false);
148 | this.subsReady = subsReady.get.bind(subsReady);
149 | ```
150 |
151 | When called from within `getReactiveState`, it sets up a dependency on that method's `Tracker.autorun` even as a bound function (which is pretty awesome).
152 |
153 | ## Future Work
154 | ### Server-side
155 | The package loads React onto server and client, but the mixins are not supported on the server since Tracker and `Meteor.subscribe` are not supported on the server. Thoughts and pull requests welcome.
156 |
--------------------------------------------------------------------------------
/examples/leaderboard/.meteor/.finished-upgraders:
--------------------------------------------------------------------------------
1 | # This file contains information which helps Meteor properly upgrade your
2 | # app when you run 'meteor update'. You should check it into version control
3 | # with your project.
4 |
5 | notices-for-0.9.0
6 | notices-for-0.9.1
7 | 0.9.4-platform-file
8 | notices-for-facebook-graph-api-2
9 |
--------------------------------------------------------------------------------
/examples/leaderboard/.meteor/.gitignore:
--------------------------------------------------------------------------------
1 | local
2 |
--------------------------------------------------------------------------------
/examples/leaderboard/.meteor/.id:
--------------------------------------------------------------------------------
1 | # This file contains a token that is unique to your project.
2 | # Check it into your repository along with the rest of this directory.
3 | # It can be used for purposes such as:
4 | # - ensuring you don't accidentally deploy one app on top of another
5 | # - providing package authors with aggregated statistics
6 |
7 | 18jw9601dypnuo1hstt7l
8 |
--------------------------------------------------------------------------------
/examples/leaderboard/.meteor/packages:
--------------------------------------------------------------------------------
1 | # Meteor packages used by this project, one per line.
2 | # Check this file (and the other files in this directory) into your repository.
3 | #
4 | # 'meteor add' and 'meteor remove' will edit this file for you,
5 | # but you can also edit it by hand.
6 |
7 | meteor-platform
8 | insecure
9 | grigio:babel
10 | grove:react
11 | accounts-ui
12 | accounts-password
13 |
--------------------------------------------------------------------------------
/examples/leaderboard/.meteor/platforms:
--------------------------------------------------------------------------------
1 | server
2 | browser
3 |
--------------------------------------------------------------------------------
/examples/leaderboard/.meteor/release:
--------------------------------------------------------------------------------
1 | METEOR@1.1.0.1
2 |
--------------------------------------------------------------------------------
/examples/leaderboard/.meteor/versions:
--------------------------------------------------------------------------------
1 | accounts-base@1.2.0
2 | accounts-password@1.1.1
3 | accounts-ui@1.1.5
4 | accounts-ui-unstyled@1.1.7
5 | autoupdate@1.2.1
6 | base64@1.0.3
7 | binary-heap@1.0.3
8 | blaze@2.1.2
9 | blaze-tools@1.0.3
10 | boilerplate-generator@1.0.3
11 | callback-hook@1.0.3
12 | check@1.0.5
13 | ddp@1.1.0
14 | deps@1.0.7
15 | ejson@1.0.6
16 | email@1.0.6
17 | fastclick@1.0.3
18 | geojson-utils@1.0.3
19 | grigio:babel@0.1.0
20 | grove:react@0.1.1
21 | html-tools@1.0.4
22 | htmljs@1.0.4
23 | http@1.1.0
24 | id-map@1.0.3
25 | insecure@1.0.3
26 | jquery@1.11.3_2
27 | json@1.0.3
28 | launch-screen@1.0.2
29 | less@1.0.14
30 | livedata@1.0.13
31 | localstorage@1.0.3
32 | logging@1.0.7
33 | meteor@1.1.6
34 | meteor-platform@1.2.2
35 | minifiers@1.1.5
36 | minimongo@1.0.8
37 | mobile-status-bar@1.0.3
38 | mongo@1.1.0
39 | npm-bcrypt@0.7.8_2
40 | observe-sequence@1.0.6
41 | ordered-dict@1.0.3
42 | random@1.0.3
43 | reactive-dict@1.1.0
44 | reactive-var@1.0.5
45 | reload@1.1.3
46 | retry@1.0.3
47 | routepolicy@1.0.5
48 | service-configuration@1.0.4
49 | session@1.1.0
50 | sha@1.0.3
51 | spacebars@1.0.6
52 | spacebars-compiler@1.0.6
53 | srp@1.0.3
54 | templating@1.1.1
55 | tracker@1.0.7
56 | ui@1.0.6
57 | underscore@1.0.3
58 | url@1.0.4
59 | webapp@1.2.0
60 | webapp-hashing@1.0.3
61 |
--------------------------------------------------------------------------------
/examples/leaderboard/README.md:
--------------------------------------------------------------------------------
1 | ## Leaderboard with `grove:react` + `grigio:babel`
--------------------------------------------------------------------------------
/examples/leaderboard/leaderboard.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
3 | font-weight: 200;
4 | margin: 50px 0;
5 | padding: 0;
6 | -webkit-user-select: none;
7 | -khtml-user-select: none;
8 | -moz-user-select: none;
9 | -o-user-select: none;
10 | user-select: none;
11 | }
12 |
13 | #outer {
14 | width: 600px;
15 | margin: 0 auto;
16 | }
17 |
18 | .player {
19 | cursor: pointer;
20 | padding: 5px;
21 | }
22 |
23 | .player .name {
24 | display: inline-block;
25 | width: 300px;
26 | font-size: 1.75em;
27 | }
28 |
29 | .player .score {
30 | display: inline-block;
31 | width: 100px;
32 | text-align: right;
33 | font-size: 2em;
34 | font-weight: bold;
35 | color: #777;
36 | }
37 |
38 | .player.selected {
39 | background-color: yellow;
40 | }
41 |
42 | .player.selected .score {
43 | color: black;
44 | }
45 |
46 | .details, .none {
47 | font-weight: bold;
48 | font-size: 2em;
49 | border-style: dashed none none none;
50 | border-color: #ccc;
51 | border-width: 4px;
52 | margin: 50px 10px;
53 | padding: 10px 0px;
54 | }
55 |
56 | .none {
57 | color: #777;
58 | }
59 |
60 | .inc {
61 | cursor: pointer;
62 | }
63 |
--------------------------------------------------------------------------------
/examples/leaderboard/leaderboard.html:
--------------------------------------------------------------------------------
1 |
2 | Leaderboard react + meteor
3 |
4 |
5 |
6 |
54 |
55 | var inputOrMessage;
56 |
57 | if (this.state.selectedName) {
58 | inputOrMessage =
59 |
{this.state.selectedName}
60 |
66 |
67 | } else {
68 | inputOrMessage =
69 | Click a player to select
70 |
71 | }
72 |
73 | return (
74 |
75 | { this.state.players.map(this.renderPlayer) }
76 | { inputOrMessage }
77 | You are {this.state.user || 'not logged in :('}
78 |
79 | )
80 |
81 | }
82 | });
83 |
84 | // Alternative to `React.createClass({...})` but no ES6 mixins support
85 | // see: https://facebook.github.io/react/docs/reusable-components.html#no-mixins
86 | class Player extends React.Component {
87 | constructor(props) {
88 | super(props);
89 | }
90 |
91 | render() {
92 | var { name, score, ...rest } = this.props;
93 | var classString = `player ${rest.className}`;
94 | return
95 | {name}
96 | {score}
97 |
;
98 | }
99 | }
100 |
101 | // Attach to a document id.
102 | Meteor.startup(function (argument) {
103 | React.render(, document.getElementById('leaderboard_placeholder'));
104 | });
105 |
106 | } // client
107 |
108 | // On server startup, create some players if the database is empty.
109 | if (Meteor.isServer) {
110 | Meteor.startup(function () {
111 | if (Players.find().count() === 0) {
112 | var names = ["Ada Lovelace",
113 | "Grace Hopper",
114 | "Marie Curie",
115 | "Carl Friedrich Gauss",
116 | "Nikola Tesla",
117 | "Claude Shannon"];
118 | for (var i = 0; i < names.length; i++) {
119 | Players.insert({
120 | name: names[i],
121 | score: Math.floor(Random.fraction()*10)*5
122 | });
123 | }
124 | }
125 | });
126 |
127 | Meteor.publish("players", function() {
128 | return Players.find();
129 | });
130 | }
131 |
--------------------------------------------------------------------------------
/package.js:
--------------------------------------------------------------------------------
1 | Package.describe({
2 | name: "grove:react",
3 | version: "0.3.0",
4 | summary: "React for Meteor - vendor files and essential mixins",
5 | git: "https://github.com/grovelabs/meteor-react/"
6 | });
7 |
8 | Package.onUse( function(api) {
9 | api.use([
10 | 'tracker@1.0.7',
11 | 'reactive-var@1.0.5',
12 | 'underscore@1.0.3'
13 | ], 'client');
14 |
15 | api.addFiles([
16 | 'src/ReactiveMixin.js',
17 | 'src/DDPMixin.js'
18 | ], 'client');
19 |
20 | api.export([
21 | 'ReactiveMixin',
22 | 'DDPMixin'
23 | ], 'client');
24 |
25 | });
--------------------------------------------------------------------------------
/src/DDPMixin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * React Mixin for binding reactive data sources and DDP subscriptions
3 | * to the state of a component using Tracker.
4 | *
5 | * Components using this mixin should implement `subscriptions`, returning
6 | * either a single subscription handle, an array of subscription handles,
7 | * or nothing. These subscriptions handles come from sources such as
8 | * `Meteor.subscribe`
9 | *
10 | * Components using this mixin have access to an additional field on the state,
11 | * `subsReady`, and a function on the component, also `subsReady`. The
12 | * difference between the two is that the `subsReady` function is reactive.
13 | * You want to use that in `getReactiveState` -- it sets up a dependency
14 | * within the implicit Tracker.autorun. You can use `subsReady` within a
15 | * components `render`, but it follows React convention more closely to use the
16 | * state variable instead.
17 | *
18 | * `this.state.subsReady` {Boolean} represents state of the declared subs
19 | * `this.subsReady` {Function} reactive method also representing state of subs
20 | *
21 | *
22 | */
23 | DDPMixin = {
24 |
25 | getInitialState: function() {
26 | var self = this;
27 | if ( self.subscriptions ) {
28 | // Setting up Tracker state
29 | var subsReady = new ReactiveVar(false);
30 | self._subsReadyVar = subsReady;
31 | // The reactive method to call in getReactiveState
32 | self.subsReady = subsReady.get.bind(subsReady);
33 |
34 | // Setting up React state
35 | return {
36 | subsReady: false
37 | };
38 | }
39 | },
40 |
41 | componentWillMount: function() {
42 | var self = this;
43 | self._subsComputation = Tracker.autorun( function(computation) {
44 | var subsReady;
45 | // If you call Meteor.subscribe within Tracker.autorun, the
46 | // subscription will be automatically cancelled when the computation
47 | // is invalidated or stopped; it's not necessary to call stop on
48 | // subscriptions made from inside autorun. However, if the next
49 | // iteration of your run function subscribes to the same record set
50 | // (same name and parameters), Meteor is smart enough to skip a
51 | // wasteful unsubscribe/resubscribe
52 | var subs = self.subscriptions();
53 | // assuming it's either undefined or DDP.subscribe handles
54 | if (typeof subs !== 'undefined') {
55 | subs = [].concat(subs); // make it an array
56 | subsReady = _.every(subs, function(sub) { return sub.ready(); });
57 | // The .ready() call is the reactive data source
58 | } else {
59 | // True if there are no subs, subscriptions() returned nothing
60 | subsReady = true;
61 | }
62 |
63 | self._subsReadyVar.set(subsReady); // Tracker
64 | self.setState({ // React
65 | subsReady: subsReady
66 | });
67 | });
68 | },
69 |
70 | componentWillUnmount: function() {
71 | if ( this._subsComputation ) {
72 | this._subsComputation.stop();
73 | this._subsComputation = null;
74 | }
75 | }
76 |
77 | };
--------------------------------------------------------------------------------
/src/ReactiveMixin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This mixin provides a way of binding reactive data sources to React
3 | * components. Components that use this mixin should implement a method
4 | * named `getReactiveState` and return an object from that method, much
5 | * like the standard `getInitialState`. Within the `getReactiveState`
6 | * function you must make sure to call on [reactive data sources](http://docs.meteor.com/#/full/reactivity)
7 | * or else the method won't rerun. See DDPMixin if you're trying to reactively
8 | * wait on DDP subscriptions to be ready
9 | *
10 | * If the subscriptions are ready immediately the `subsReady` state will be
11 | * updated synchronously, reflecting that state at `componentWillMount`
12 | */
13 | ReactiveMixin = {
14 |
15 | getInitialState: function() {
16 | var self = this;
17 | if ( self.getReactiveState ) {
18 | var initState = {};
19 | // Log a console warning if mixins are in the wrong order
20 | if (this.subscriptions && typeof this.subsReady === 'undefined' ) {
21 | console.warn('Need to bring in DDPMixin before ReactiveMixin to define the subscription state');
22 | }
23 | self._reactiveStateComputation = Tracker.autorun( function(computation) {
24 | // Something in getReactiveState MUST be a reactive data source
25 | // in order for rerun
26 | var reactiveState = self.getReactiveState();
27 | if ( typeof reactiveState !== 'undefined' ) {
28 | if (computation.firstRun) {
29 | initState = reactiveState;
30 | } else if ( self.isMounted() ) {
31 | // can't call setState until component is mounted
32 | self.setState(reactiveState);
33 | } else { // it's not mounted and we need to wait to `setState`
34 | Tracker.afterFlush(function () {
35 | self.setState(reactiveState); // set async
36 | });
37 | }
38 | }
39 | });
40 | return initState;
41 | }
42 | },
43 |
44 | componentWillUnmount: function() {
45 | if (this._reactiveStateComputation) {
46 | this._reactiveStateComputation.stop();
47 | this._reactiveStateComputation = null;
48 | }
49 | }
50 |
51 | };
--------------------------------------------------------------------------------