diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..ddfbeca
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,23 @@
+{
+ "extends": "airbnb",
+ "parser": "babel-eslint",
+ "rules": {
+ "id-length": [2, {
+ "exceptions": ["_"]
+ }],
+ "arrow-body-style": 0,
+ "camelcase": 0,
+ "consistent-return": 0,
+ "curly": 0,
+ "func-names": 0,
+ "new-cap": 0,
+ "no-param-reassign": 0,
+ "prefer-arrow-callback": 0,
+ "space-before-function-paren": 0,
+ "react/no-multi-comp": [2, {
+ "ignoreStateless": true
+ }],
+ "react/prefer-stateless-function": 0,
+ "react/jsx-no-bind": 0
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..40b878d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+node_modules/
\ No newline at end of file
diff --git a/.meteor/.finished-upgraders b/.meteor/.finished-upgraders
new file mode 100644
index 0000000..dacc2c0
--- /dev/null
+++ b/.meteor/.finished-upgraders
@@ -0,0 +1,13 @@
+# This file contains information which helps Meteor properly upgrade your
+# app when you run 'meteor update'. You should check it into version control
+# with your project.
+
+notices-for-0.9.0
+notices-for-0.9.1
+0.9.4-platform-file
+notices-for-facebook-graph-api-2
+1.2.0-standard-minifiers-package
+1.2.0-meteor-platform-split
+1.2.0-cordova-changes
+1.2.0-breaking-changes
+1.3.0-split-minifiers-package
diff --git a/.meteor/.gitignore b/.meteor/.gitignore
new file mode 100644
index 0000000..4083037
--- /dev/null
+++ b/.meteor/.gitignore
@@ -0,0 +1 @@
+local
diff --git a/.meteor/.id b/.meteor/.id
new file mode 100644
index 0000000..6ad3cf0
--- /dev/null
+++ b/.meteor/.id
@@ -0,0 +1,7 @@
+# This file contains a token that is unique to your project.
+# Check it into your repository along with the rest of this directory.
+# It can be used for purposes such as:
+# - ensuring you don't accidentally deploy one app on top of another
+# - providing package authors with aggregated statistics
+
+vq6rq61988jma1l59dr2
diff --git a/.meteor/packages b/.meteor/packages
new file mode 100644
index 0000000..73b0300
--- /dev/null
+++ b/.meteor/packages
@@ -0,0 +1,20 @@
+# Meteor packages used by this project, one per line.
+# Check this file (and the other files in this directory) into your repository.
+#
+# 'meteor add' and 'meteor remove' will edit this file for you,
+# but you can also edit it by hand.
+
+meteor-base # Packages every Meteor app needs to have
+mobile-experience # Packages for a great mobile UX
+mongo # The database Meteor supports right now
+blaze-html-templates # Compile .html files into Meteor Blaze views
+reactive-var # Reactive variable for tracker
+jquery # Helpful client-side library
+tracker # Meteor's client-side reactive programming library
+
+standard-minifier-css # CSS minifier run for production mode
+standard-minifier-js # JS minifier run for production mode
+es5-shim # ECMAScript 5 compatibility for older browsers.
+ecmascript # Enable ECMAScript2015+ syntax in app code
+
+simple:dev-error-overlay
diff --git a/.meteor/platforms b/.meteor/platforms
new file mode 100644
index 0000000..efeba1b
--- /dev/null
+++ b/.meteor/platforms
@@ -0,0 +1,2 @@
+server
+browser
diff --git a/.meteor/release b/.meteor/release
new file mode 100644
index 0000000..ef5046b
--- /dev/null
+++ b/.meteor/release
@@ -0,0 +1 @@
+METEOR@1.3.1
diff --git a/.meteor/versions b/.meteor/versions
new file mode 100644
index 0000000..d30b874
--- /dev/null
+++ b/.meteor/versions
@@ -0,0 +1,69 @@
+allow-deny@1.0.3
+autoupdate@1.2.7
+babel-compiler@6.6.1
+babel-runtime@0.1.7
+base64@1.0.7
+binary-heap@1.0.7
+blaze@2.1.6
+blaze-html-templates@1.0.3
+blaze-tools@1.0.7
+boilerplate-generator@1.0.7
+caching-compiler@1.0.3
+caching-html-compiler@1.0.5
+callback-hook@1.0.7
+check@1.1.3
+ddp@1.2.4
+ddp-client@1.2.4
+ddp-common@1.2.4
+ddp-server@1.2.5
+deps@1.0.11
+diff-sequence@1.0.4
+ecmascript@0.4.2
+ecmascript-runtime@0.2.9
+ejson@1.0.10
+es5-shim@4.5.9
+fastclick@1.0.10
+geojson-utils@1.0.7
+hot-code-push@1.0.3
+html-tools@1.0.8
+htmljs@1.0.8
+http@1.1.4
+id-map@1.0.6
+jquery@1.11.7
+launch-screen@1.0.10
+less@2.5.7
+livedata@1.0.17
+logging@1.0.11
+meteor@1.1.13
+meteor-base@1.0.3
+minifier-css@1.1.10
+minifier-js@1.1.10
+minimongo@1.0.13
+mobile-experience@1.0.3
+mobile-status-bar@1.0.11
+modules@0.5.2
+modules-runtime@0.6.2
+mongo@1.1.6
+mongo-id@1.0.3
+npm-mongo@1.4.42
+observe-sequence@1.0.10
+ordered-dict@1.0.6
+promise@0.6.6
+random@1.0.8
+reactive-var@1.0.8
+reload@1.1.7
+retry@1.0.6
+routepolicy@1.0.9
+simple:dev-error-overlay@1.5.1
+spacebars@1.0.10
+spacebars-compiler@1.0.10
+standard-minifier-css@1.0.5
+standard-minifier-js@1.0.5
+templating@1.1.8
+templating-tools@1.0.3
+tracker@1.0.12
+ui@1.0.10
+underscore@1.0.7
+url@1.0.8
+webapp@1.2.7
+webapp-hashing@1.0.8
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..50d0078
--- /dev/null
+++ b/README.md
@@ -0,0 +1,6 @@
+Meteor Redux Demo
+=================
+
+This repository contains a demonstration application for the article [A bridge between React and Meteor](https://subvisual.co/blog/posts/79-working-with-meteor-react-and-redux). In this application you can write and see messages. If you try to write an empty message you'll see an error.
+
+The purpose of this application is to demonstrate a possible setup for Redux on Meteor, most code written here is not acceptable on production application.
diff --git a/client/actions/actions.js b/client/actions/actions.js
new file mode 100644
index 0000000..cad0b33
--- /dev/null
+++ b/client/actions/actions.js
@@ -0,0 +1,14 @@
+import { Meteor } from 'meteor/meteor';
+
+export const createMessage = params => {
+ return dispatch => {
+ Meteor.call('createMessage', params, (error) => {
+ if (!error) return;
+
+ dispatch({
+ type: 'ADD_ERROR',
+ error,
+ });
+ });
+ };
+};
diff --git a/client/components/Errors/Errors.jsx b/client/components/Errors/Errors.jsx
new file mode 100644
index 0000000..b394830
--- /dev/null
+++ b/client/components/Errors/Errors.jsx
@@ -0,0 +1,17 @@
+import React, { Component, PropTypes } from 'react';
+
+class ErrorsList extends Component {
+ renderError(error, index) {
+ return
{error.message};
+ }
+
+ render() {
+ return {this.props.errors.map(this.renderError)}
;
+ }
+}
+
+ErrorsList.propTypes = {
+ errors: PropTypes.array.isRequired,
+};
+
+export default ErrorsList;
diff --git a/client/components/MessagesEditor/MessagesEditor.jsx b/client/components/MessagesEditor/MessagesEditor.jsx
new file mode 100644
index 0000000..f29f62a
--- /dev/null
+++ b/client/components/MessagesEditor/MessagesEditor.jsx
@@ -0,0 +1,27 @@
+import React, { Component, PropTypes } from 'react';
+import { reduxForm } from 'redux-form';
+
+const fields = ['text'];
+
+class MessagesEditor extends Component {
+ render() {
+ const { fields: { text }, handleSubmit } = this.props;
+
+ return (
+
+ );
+ }
+}
+
+MessagesEditor.propTypes = {
+ fields: PropTypes.object.isRequired,
+ handleSubmit: PropTypes.func.isRequired,
+};
+
+export default reduxForm({
+ fields,
+ form: 'new-message',
+})(MessagesEditor);
diff --git a/client/components/MessagesList/MessagesList.jsx b/client/components/MessagesList/MessagesList.jsx
new file mode 100644
index 0000000..5490415
--- /dev/null
+++ b/client/components/MessagesList/MessagesList.jsx
@@ -0,0 +1,17 @@
+import React, { Component, PropTypes } from 'react';
+
+class MessagesList extends Component {
+ renderMessage(message, index) {
+ return {message.text};
+ }
+
+ render() {
+ return {this.props.messages.map(this.renderMessage)}
;
+ }
+}
+
+MessagesList.propTypes = {
+ messages: PropTypes.array.isRequired,
+};
+
+export default MessagesList;
diff --git a/client/containers/App.jsx b/client/containers/App.jsx
new file mode 100644
index 0000000..ece3ce9
--- /dev/null
+++ b/client/containers/App.jsx
@@ -0,0 +1,51 @@
+import React, { Component, PropTypes } from 'react';
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+import { createMessage } from '../actions/actions';
+import { reset } from 'redux-form';
+
+import SubscribeComponent from '../helpers/SubscribeComponent';
+import MessagesList from '../components/MessagesList/MessagesList';
+import MessagesEditor from '../components/MessagesEditor/MessagesEditor';
+import Errors from '../components/Errors/Errors';
+
+class App extends Component {
+ componentWillMount() {
+ this.props.subscribe('messages');
+ }
+
+ handleSubmit(fields) {
+ this.props.createMessage(fields);
+ this.props.reset('new-message');
+ }
+
+ render() {
+ return (
+
+
+
+
+
+ );
+ }
+}
+
+App.propTypes = {
+ subscribe: PropTypes.func.isRequired,
+ createMessage: PropTypes.func.isRequired,
+ reset: PropTypes.func.isRequired,
+ messages: PropTypes.array.isRequired,
+ errors: PropTypes.array.isRequired,
+};
+
+const mapStateToProps = state => {
+ return { messages: state.messages, errors: state.errors };
+};
+
+const mapDispatchToProps = dispatch => {
+ return bindActionCreators({ createMessage, reset }, dispatch);
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(SubscribeComponent(App));
diff --git a/client/helpers/SubscribeComponent.jsx b/client/helpers/SubscribeComponent.jsx
new file mode 100644
index 0000000..f924d71
--- /dev/null
+++ b/client/helpers/SubscribeComponent.jsx
@@ -0,0 +1,35 @@
+import { Meteor } from 'meteor/meteor';
+import React, { Component } from 'react';
+
+export default ComposedComponent => class extends Component {
+ constructor() {
+ super();
+ this.subs = {};
+ }
+
+ subscribe(name, ...args) {
+ if (this.subs[name])
+ this.subs[name].stop();
+
+ this.subs[name] = Meteor.subscribe(name, ...args);
+ }
+
+ subscriptionReady(name) {
+ if (this.subs[name].ready())
+ return this.subs[name].ready();
+ }
+
+ componentWillUnmount() {
+ Object.keys(this.subs).map(key => this.subs[key].stop());
+ }
+
+ render() {
+ return (
+
+ );
+ }
+};
diff --git a/client/main.html b/client/main.html
new file mode 100644
index 0000000..4082f27
--- /dev/null
+++ b/client/main.html
@@ -0,0 +1,6 @@
+
+ Meteor Redux Demo
+
+
+
+
diff --git a/client/main.js b/client/main.js
new file mode 100644
index 0000000..8b84892
--- /dev/null
+++ b/client/main.js
@@ -0,0 +1,18 @@
+import { Meteor } from 'meteor/meteor';
+import React from 'react';
+import { render } from 'react-dom';
+import { Provider } from 'react-redux';
+import { Router, Route, browserHistory } from 'react-router';
+
+import createStore from './store/createStore';
+import App from './containers/App';
+
+Meteor.startup(() => {
+ render((
+
+
+
+
+
+ ), document.getElementById('app'));
+});
diff --git a/client/reducers/reducers.js b/client/reducers/reducers.js
new file mode 100644
index 0000000..526ba12
--- /dev/null
+++ b/client/reducers/reducers.js
@@ -0,0 +1,26 @@
+import { combineReducers } from 'redux';
+import { reducer as formReducer } from 'redux-form';
+
+const messagesReducer = (state = [], action) => {
+ switch (action.type) {
+ case 'SET_MESSAGES':
+ return action.messages;
+ default:
+ return state;
+ }
+};
+
+const errorsReducer = (state = [], action) => {
+ switch (action.type) {
+ case 'ADD_ERROR':
+ return [...state, action.error];
+ default:
+ return state;
+ }
+};
+
+export default combineReducers({
+ messages: messagesReducer,
+ errors: errorsReducer,
+ form: formReducer,
+});
diff --git a/client/store/createStore.js b/client/store/createStore.js
new file mode 100644
index 0000000..c9a37cd
--- /dev/null
+++ b/client/store/createStore.js
@@ -0,0 +1,19 @@
+import { Tracker } from 'meteor/tracker';
+import { createStore, applyMiddleware } from 'redux';
+import thunk from 'redux-thunk';
+
+import reducers from '../reducers/reducers';
+import Messages from '../../lib/messages';
+
+export default () => {
+ const store = createStore(reducers, applyMiddleware(thunk));
+
+ Tracker.autorun(() => {
+ store.dispatch({
+ type: 'SET_MESSAGES',
+ messages: Messages.find().fetch(),
+ });
+ });
+
+ return store;
+};
diff --git a/lib/messages.jsx b/lib/messages.jsx
new file mode 100644
index 0000000..5e17472
--- /dev/null
+++ b/lib/messages.jsx
@@ -0,0 +1,5 @@
+import { Mongo } from 'meteor/mongo';
+
+Messages = new Mongo.Collection('messages');
+
+export default Messages;
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..180344c
--- /dev/null
+++ b/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "meteor-redux-demo",
+ "private": true,
+ "scripts": {
+ "start": "meteor run"
+ },
+ "dependencies": {
+ "lodash": "^4.11.1",
+ "meteor-node-stubs": "~0.2.0",
+ "react": "^15.0.1",
+ "react-dom": "^15.0.1",
+ "react-redux": "^4.4.5",
+ "react-router": "^2.2.2",
+ "redux": "^3.4.0",
+ "redux-form": "^5.0.1",
+ "redux-thunk": "^2.0.1"
+ },
+ "devDependencies": {
+ "babel-eslint": "^6.0.2",
+ "eslint": "^2.7.0",
+ "eslint-config-airbnb": "^6.2.0",
+ "eslint-config-shakacode": "^4.0.0",
+ "eslint-plugin-react": "^4.2.3"
+ }
+}
diff --git a/server/main.js b/server/main.js
new file mode 100644
index 0000000..b8f34e0
--- /dev/null
+++ b/server/main.js
@@ -0,0 +1,2 @@
+import './publications';
+import './methods';
diff --git a/server/methods.js b/server/methods.js
new file mode 100644
index 0000000..0c3af96
--- /dev/null
+++ b/server/methods.js
@@ -0,0 +1,13 @@
+import { Meteor } from 'meteor/meteor';
+import Messages from '../lib/messages';
+
+const createMessage = function(params) {
+ if (!params.text)
+ throw new Meteor.Error('text missing', 'Cannot submit an empty message');
+
+ Messages.insert(params);
+};
+
+Meteor.methods({
+ createMessage,
+});
diff --git a/server/publications.js b/server/publications.js
new file mode 100644
index 0000000..68faf87
--- /dev/null
+++ b/server/publications.js
@@ -0,0 +1,6 @@
+import { Meteor } from 'meteor/meteor';
+import Messages from '../lib/messages';
+
+Meteor.publish('messages', function() {
+ return Messages.find({});
+});