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 ; + } +} + +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 ; + } +} + +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({}); +});