├── server
├── .meteor
│ ├── release
│ ├── platforms
│ ├── .gitignore
│ ├── .id
│ ├── .finished-upgraders
│ ├── packages
│ └── versions
├── server
│ └── main.js
├── imports
│ └── server
│ │ ├── startup
│ │ └── index.js
│ │ └── api
│ │ └── todos
│ │ ├── todos.js
│ │ ├── publications.js
│ │ └── methods.js
└── package.json
├── client
├── .gitignore
├── .babelrc
├── src
│ ├── app.js
│ ├── index.html
│ ├── configure-asteroid.js
│ ├── components
│ │ ├── fetch-error.js
│ │ ├── todo-app.js
│ │ ├── root.js
│ │ ├── filter-link.js
│ │ ├── footer.js
│ │ ├── todo.js
│ │ ├── add-todo.js
│ │ └── todo-list.js
│ ├── actions
│ │ ├── asteroid.js
│ │ └── index.js
│ ├── configure-store.js
│ └── reducers
│ │ ├── byId.js
│ │ ├── index.js
│ │ └── createList.js
├── .eslintrc
├── webpack.config.js
└── package.json
├── LICENSE
└── README.md
/server/.meteor/release:
--------------------------------------------------------------------------------
1 | METEOR@1.4.0.1
2 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 |
--------------------------------------------------------------------------------
/server/.meteor/platforms:
--------------------------------------------------------------------------------
1 | server
2 | browser
3 |
--------------------------------------------------------------------------------
/server/.meteor/.gitignore:
--------------------------------------------------------------------------------
1 | local
2 | dev_bundle
3 |
--------------------------------------------------------------------------------
/server/server/main.js:
--------------------------------------------------------------------------------
1 | import '/imports/server/startup'
2 |
--------------------------------------------------------------------------------
/client/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [["es2015", {"modules": false}], "react", "stage-2"]
3 | }
4 |
--------------------------------------------------------------------------------
/server/imports/server/startup/index.js:
--------------------------------------------------------------------------------
1 | import '../api/todos/methods.js'
2 | import '../api/todos/publications.js'
3 |
--------------------------------------------------------------------------------
/client/src/app.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import Root from './components/root'
4 |
5 | ReactDOM.render(
6 | ,
7 | document.getElementById('root')
8 | )
9 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "private": true,
4 | "scripts": {
5 | "start": "meteor run"
6 | },
7 | "dependencies": {
8 | "meteor-node-stubs": "~0.2.0"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/client/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | React Redux Playground
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/client/src/configure-asteroid.js:
--------------------------------------------------------------------------------
1 | import { createClass } from 'asteroid'
2 |
3 | const Asteroid = createClass()
4 | // Connect to a Meteor backend
5 | const asteroid = new Asteroid({
6 | endpoint: 'ws://localhost:3000/websocket',
7 | })
8 |
9 | export default asteroid
10 |
--------------------------------------------------------------------------------
/client/src/components/fetch-error.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const FetchError = ({ message, onRetry }) => (
4 |
5 |
Could not fetch Todos, {message}
6 |
7 |
8 | )
9 |
10 | export default FetchError
11 |
--------------------------------------------------------------------------------
/client/src/components/todo-app.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import TodoList from './todo-list'
3 | import Footer from './footer'
4 | import AddTodo from './add-todo'
5 |
6 | const TodoApp = () => (
7 |
12 | )
13 |
14 | export default TodoApp
15 |
--------------------------------------------------------------------------------
/server/.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 | g6430u1f326z56cd0vy
8 |
--------------------------------------------------------------------------------
/client/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true
4 | },
5 | "parserOptions": {
6 | "ecmaVersion": 6,
7 | "sourceType": "module",
8 | "ecmaFeatures": {
9 | "jsx": true,
10 | "experimentalObjectRestSpread": true
11 | }
12 | },
13 | "extends": "airbnb",
14 | "plugins": ["react"],
15 | "rules": {
16 | semi: ["error", "never"]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/server/imports/server/api/todos/todos.js:
--------------------------------------------------------------------------------
1 | import { Mongo } from 'meteor/mongo'
2 | import { SimpleSchema } from 'meteor/aldeed:simple-schema'
3 |
4 | const Todos = new Mongo.Collection('todos')
5 |
6 | Todos.schema = new SimpleSchema({
7 | _id: {
8 | type: String,
9 | },
10 | text: {
11 | type: String,
12 | },
13 | completed: {
14 | type: Boolean,
15 | defaultValue: false,
16 | },
17 | })
18 |
19 | Todos.attachSchema(Todos.schema)
20 |
21 | export default Todos
22 |
--------------------------------------------------------------------------------
/client/src/components/root.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Provider } from 'react-redux'
3 | import { Router, Route, browserHistory } from 'react-router'
4 | import TodoApp from './todo-app'
5 | import configureStore from '../configure-store'
6 |
7 | const Root = () => (
8 |
9 |
10 |
11 |
12 |
13 | )
14 |
15 | export default Root
16 |
--------------------------------------------------------------------------------
/client/src/components/filter-link.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 | import { Link } from 'react-router'
3 |
4 | const FilterLink = ({ filter, children }) => (
5 |
12 | {children}
13 |
14 | )
15 |
16 | FilterLink.propTypes = {
17 | filter: PropTypes.string.isRequired,
18 | children: PropTypes.any,
19 | }
20 |
21 | export default FilterLink
22 |
--------------------------------------------------------------------------------
/client/src/components/footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import FilterLink from './filter-link'
3 |
4 | const Footer = () => (
5 |
6 | {'Show'}
7 |
10 | {'All'}
11 |
12 | {' '}
13 |
16 | {'Active'}
17 |
18 | {' '}
19 |
22 | {'Completed'}
23 |
24 |
25 | )
26 |
27 | export default Footer
28 |
--------------------------------------------------------------------------------
/server/imports/server/api/todos/publications.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor'
2 | import Todos from './todos'
3 |
4 | Meteor.publish('Todos.list', (filter = 'all') => {
5 | const query = {}
6 | // simulate slow server
7 | Meteor._sleepForMs(1000)
8 | // simulate error when publishing
9 | // throw new Meteor.Error('Something went wrong!')
10 | if (filter === 'active') {
11 | query.completed = false
12 | }
13 | else if (filter === 'completed') {
14 | query.completed = true
15 | }
16 | return Todos.find(query)
17 | })
18 |
--------------------------------------------------------------------------------
/server/.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 | 1.2.0-standard-minifiers-package
10 | 1.2.0-meteor-platform-split
11 | 1.2.0-cordova-changes
12 | 1.2.0-breaking-changes
13 | 1.3.0-split-minifiers-package
14 | 1.3.5-remove-old-dev-bundle-link
15 | 1.4.0-remove-old-dev-bundle-link
16 |
--------------------------------------------------------------------------------
/client/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | module.exports = env => {
3 | return {
4 | entry: './app.js',
5 | output: {
6 | filename: 'bundle.js',
7 | path: path.resolve(__dirname, 'dist'),
8 | pathinfo: !env.prod,
9 | },
10 | context: path.resolve(__dirname, 'src'),
11 | devtool: env.prod ? 'source-map' : 'eval',
12 | bail: env.prod,
13 | module: {
14 | loaders: [
15 | { test: /\.js$/, loader: 'babel', exclude: /node_modules/ },
16 | { test: /\.json$/, loader: 'json' },
17 | ],
18 | },
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/client/src/components/todo.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 |
3 | const Todo = ({ todo, onToggleTodo }) => {
4 | const handleToggleTodo = () => {
5 | onToggleTodo(todo.id)
6 | }
7 | return (
8 |
15 | {todo.text}
16 |
17 | )
18 | }
19 |
20 | Todo.propTypes = {
21 | todo: PropTypes.object.isRequired,
22 | onToggleTodo: PropTypes.func.isRequired,
23 | }
24 |
25 | export default Todo
26 |
--------------------------------------------------------------------------------
/client/src/actions/asteroid.js:
--------------------------------------------------------------------------------
1 | export default (dispatch, asteroid) => {
2 | asteroid.ddp.on('added', ({ collection, fields, id }) => {
3 | dispatch({
4 | type: 'DDP_ADDED',
5 | response: { collection, doc: { id, ...fields } },
6 | })
7 | })
8 |
9 | asteroid.ddp.on('changed', ({ collection, fields, id }) => {
10 | dispatch({
11 | type: 'DDP_CHANGED',
12 | response: { collection, doc: { id, ...fields } },
13 | })
14 | })
15 |
16 | asteroid.ddp.on('removed', ({ collection, id }) => {
17 | dispatch({
18 | type: 'DDP_REMOVED',
19 | response: { collection, id },
20 | })
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/client/src/components/add-todo.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 | import { connect } from 'react-redux'
3 | import { addTodo } from '../actions'
4 |
5 | const AddTodo = ({ dispatch }) => {
6 | const handleAddTodo = () => {
7 | dispatch(addTodo(this.input.value))
8 | this.input.value = ''
9 | }
10 | return (
11 |
12 | {
15 | this.input = node
16 | }}
17 | />
18 |
19 |
20 | )
21 | }
22 |
23 | AddTodo.propTypes = {
24 | dispatch: PropTypes.func.isRequired,
25 | }
26 |
27 | export default connect()(AddTodo)
28 |
--------------------------------------------------------------------------------
/server/imports/server/api/todos/methods.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor'
2 | import Todos from './todos'
3 |
4 | Meteor.methods({
5 | addTodo(_id, text) {
6 | // simulate slow server
7 | Meteor._sleepForMs(1000)
8 |
9 | // simulate add todo error
10 | // throw Error('NOT ALLOWED!!!')
11 | const todoId = Todos.insert({ _id, text })
12 | return Todos.findOne(todoId)
13 | },
14 | toggleTodo(_id) {
15 | // simulate slow server
16 | Meteor._sleepForMs(1000)
17 |
18 | // simulate toggle todo error
19 | // throw Error('NOT ALLOWED!!!')
20 | const todo = Todos.findOne(_id)
21 | Todos.update(_id, { $set: { completed: !todo.completed } })
22 | }
23 | })
24 |
--------------------------------------------------------------------------------
/client/src/configure-store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux'
2 | import createLogger from 'redux-logger'
3 | import reducers from './reducers/'
4 | import thunk from 'redux-thunk'
5 | import asteroid from './configure-asteroid'
6 | import initializeListeners from './actions/asteroid'
7 |
8 | const configureStore = () => {
9 | const middlewares = [thunk.withExtraArgument(asteroid)]
10 |
11 | if (process.env.NODE_ENV !== 'production') {
12 | middlewares.push(createLogger())
13 | }
14 |
15 | const store = createStore(
16 | reducers,
17 | applyMiddleware(...middlewares)
18 | )
19 |
20 | initializeListeners(store.dispatch, asteroid)
21 |
22 | return store
23 | }
24 |
25 | export default configureStore
26 |
--------------------------------------------------------------------------------
/client/src/reducers/byId.js:
--------------------------------------------------------------------------------
1 | import omit from 'lodash/omit'
2 |
3 | const byId = (state = {}, action) => {
4 | if (action.type === 'DDP_CHANGED') {
5 | return {
6 | ...state,
7 | [action.response.doc.id]: {
8 | // merge the old doc with the changed fields
9 | ...state[action.response.doc.id],
10 | ...action.response.doc,
11 | },
12 | }
13 | }
14 | if (action.type === 'DDP_ADDED') {
15 | return {
16 | ...state,
17 | [action.response.doc.id]: action.response.doc,
18 | }
19 | }
20 | if (action.type === 'DDP_REMOVED') {
21 | return omit(state, action.response.id)
22 | }
23 | return state
24 | }
25 |
26 | export default byId
27 |
28 | export const getTodo = (state, id) => state[id]
29 |
--------------------------------------------------------------------------------
/client/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import byId, * as fromById from './byId'
3 | import createList, * as fromList from './createList'
4 |
5 | const listByFilter = combineReducers({
6 | all: createList('all'),
7 | active: createList('active'),
8 | completed: createList('completed'),
9 | })
10 |
11 | const todos = combineReducers({
12 | byId,
13 | listByFilter,
14 | })
15 |
16 | export default todos
17 |
18 |
19 | export const getVisibleTodos = (state, filter) => {
20 | const ids = fromList.getIds(state.listByFilter[filter])
21 | return ids.map(id => fromById.getTodo(state.byId, id))
22 | }
23 |
24 | export const getTodo = (state, id) =>
25 | fromById.getTodo(state.byId, id)
26 |
27 | export const getIsFetching = (state, filter) =>
28 | fromList.getIsFetching(state.listByFilter[filter])
29 |
30 | export const getErrorMessage = (state, filter) =>
31 | fromList.getErrorMessage(state.listByFilter[filter])
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Philipp Sporrer
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/server/.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-base@1.0.4 # Packages every Meteor app needs to have
8 | mobile-experience@1.0.4 # Packages for a great mobile UX
9 | mongo@1.1.10 # The database Meteor supports right now
10 | blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views
11 | reactive-var@1.0.10 # Reactive variable for tracker
12 | jquery@1.11.9 # Helpful client-side library
13 | tracker@1.1.0 # Meteor's client-side reactive programming library
14 |
15 | standard-minifier-css@1.1.8 # CSS minifier run for production mode
16 | standard-minifier-js@1.1.8 # JS minifier run for production mode
17 | es5-shim@4.6.13 # ECMAScript 5 compatibility for older browsers.
18 | ecmascript@0.5.7 # Enable ECMAScript2015+ syntax in app code
19 |
20 | insecure@1.0.7 # Allow all DB writes from clients (for prototyping)
21 | aldeed:collection2
22 | aldeed:simple-schema
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-redux-meteor
2 |
3 | A demo for using meteor (only) as your backend and react-redux as your frontend.
4 |
5 | I wrote a [blog post](https://blog.backlogapp.io/building-real-time-react-applications-with-redux-and-meteor-de29042eb460?gi=e166269ad5bb#.z8j0khk59) about this setup, which explains the setps you need to take to make your react-redux app real-time by connecting it to a meteor backend.
6 |
7 | The starting point for this example was the Todos App from Dan Abramov's ["Building React Applications with Idiomatic Redux"](https://egghead.io/courses/building-react-applications-with-idiomatic-redux) course ([Source code here](https://github.com/gaearon/todos)). If you are not sure how Redux works, watch the egghead.io courses first 🤓
8 |
9 | ## Features
10 |
11 | - React & Redux Frontend without any restrictions (no Meteor in the frontend)
12 | - live data *"for free"* by using a Meteor backend
13 | - optimistic UI
14 | - webpack (even in version 2.0 with tree-shaking enabled 🎉)
15 | - [Asteroid](https://github.com/mondora/asteroid) as DDP library
16 |
17 | ## How to run
18 |
19 | This app consists of a client and a server, we need to start both separately. This might sound trivial to you if you don't come from meteor 😉.
20 |
21 | ### Starting the client
22 |
23 | This will start your a `webpack-dev-server` so you can access your frontend at `http://localhost:8080`
24 | ```sh
25 | cd client
26 | npm i
27 | npm start
28 | ```
29 |
30 | ### Starting the backend
31 |
32 | To start the meteor backend you first need to install meteor if you haven't already:
33 | ```sh
34 | curl https://install.meteor.com/ | sh
35 | ```
36 | Then you can start your backend by simply running
37 | ```sh
38 | cd server
39 | meteor
40 | ```
41 |
--------------------------------------------------------------------------------
/server/.meteor/versions:
--------------------------------------------------------------------------------
1 | aldeed:collection2@2.9.1
2 | aldeed:collection2-core@1.1.1
3 | aldeed:schema-deny@1.0.1
4 | aldeed:schema-index@1.0.1
5 | aldeed:simple-schema@1.5.3
6 | allow-deny@1.0.5
7 | autoupdate@1.2.11
8 | babel-compiler@6.9.0
9 | babel-runtime@0.1.10
10 | base64@1.0.9
11 | binary-heap@1.0.9
12 | blaze@2.1.8
13 | blaze-html-templates@1.0.4
14 | blaze-tools@1.0.9
15 | boilerplate-generator@1.0.9
16 | caching-compiler@1.0.6
17 | caching-html-compiler@1.0.6
18 | callback-hook@1.0.9
19 | check@1.2.3
20 | ddp@1.2.5
21 | ddp-client@1.2.9
22 | ddp-common@1.2.6
23 | ddp-server@1.2.10
24 | deps@1.0.12
25 | diff-sequence@1.0.6
26 | ecmascript@0.5.7
27 | ecmascript-runtime@0.3.12
28 | ejson@1.0.12
29 | es5-shim@4.6.13
30 | fastclick@1.0.12
31 | geojson-utils@1.0.9
32 | hot-code-push@1.0.4
33 | html-tools@1.0.10
34 | htmljs@1.0.10
35 | http@1.1.8
36 | id-map@1.0.8
37 | insecure@1.0.7
38 | jquery@1.11.9
39 | launch-screen@1.0.12
40 | livedata@1.0.18
41 | logging@1.1.14
42 | mdg:validation-error@0.2.0
43 | meteor@1.2.16
44 | meteor-base@1.0.4
45 | minifier-css@1.2.13
46 | minifier-js@1.2.13
47 | minimongo@1.0.17
48 | mobile-experience@1.0.4
49 | mobile-status-bar@1.0.12
50 | modules@0.7.5
51 | modules-runtime@0.7.5
52 | mongo@1.1.10
53 | mongo-id@1.0.5
54 | npm-mongo@1.5.45
55 | observe-sequence@1.0.12
56 | ordered-dict@1.0.8
57 | promise@0.8.3
58 | raix:eventemitter@0.1.3
59 | random@1.0.10
60 | reactive-var@1.0.10
61 | reload@1.1.10
62 | retry@1.0.8
63 | routepolicy@1.0.11
64 | spacebars@1.0.12
65 | spacebars-compiler@1.0.12
66 | standard-minifier-css@1.1.8
67 | standard-minifier-js@1.1.8
68 | templating@1.1.14
69 | templating-tools@1.0.4
70 | tracker@1.1.0
71 | ui@1.0.11
72 | underscore@1.0.9
73 | url@1.0.10
74 | webapp@1.3.10
75 | webapp-hashing@1.0.9
76 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-playground",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "webpack.config.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "lint": "eslint src",
9 | "validate": "npm-run-all --parallel validate-webpack:* lint",
10 | "validate-webpack:dev": "webpack-validator webpack.config.js --env.dev",
11 | "validate-webpack:prod": "webpack-validator webpack.config.js --env.prod",
12 | "clean-dist": "rimraf dist",
13 | "copy-files": "cpy src/index.html dist",
14 | "clean-and-copy": "npm run clean-dist && npm run copy-files",
15 | "prebuild": "npm run clean-and-copy",
16 | "prebuild:prod": "npm run clean-and-copy",
17 | "build": "webpack --env.dev",
18 | "build:prod": "webpack --env.prod -p",
19 | "prestart": "npm run clean-and-copy",
20 | "start": "webpack-dev-server --env.dev --history-api-fallback --content-base dist"
21 | },
22 | "keywords": [],
23 | "author": "Philipp Sporrer ",
24 | "license": "MIT",
25 | "devDependencies": {
26 | "babel-core": "6.13.2",
27 | "babel-loader": "6.2.4",
28 | "babel-preset-es2015": "6.13.2",
29 | "babel-preset-react": "6.11.1",
30 | "babel-preset-stage-2": "6.11.0",
31 | "cpy-cli": "1.0.1",
32 | "eslint": "2.13.1",
33 | "eslint-config-airbnb": "9.0.1",
34 | "eslint-plugin-import": "1.11.1",
35 | "eslint-plugin-jsx-a11y": "1.5.5",
36 | "eslint-plugin-react": "5.2.2",
37 | "json-loader": "0.5.4",
38 | "npm-run-all": "2.3.0",
39 | "rimraf": "2.5.4",
40 | "webpack": "2.1.0-beta.20",
41 | "webpack-dev-server": "2.1.0-beta.0",
42 | "webpack-validator": "2.2.3"
43 | },
44 | "dependencies": {
45 | "asteroid": "2.0.2",
46 | "lodash": "4.14.1",
47 | "meteor-random": "0.0.3",
48 | "node-uuid": "1.4.7",
49 | "normalizr": "2.2.1",
50 | "react": "15.2.1",
51 | "react-dom": "15.2.1",
52 | "react-redux": "4.4.5",
53 | "react-router": "3.0.0-alpha.2",
54 | "redux": "3.5.2",
55 | "redux-logger": "2.6.1",
56 | "redux-promise": "0.5.3",
57 | "redux-thunk": "2.1.0"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/client/src/actions/index.js:
--------------------------------------------------------------------------------
1 | import random from 'meteor-random'
2 | import { getIsFetching, getTodo } from '../reducers'
3 |
4 | export const fetchTodos = (filter) => (dispatch, getState, asteroid) => {
5 | if (getIsFetching(getState(), filter)) {
6 | return Promise.resolve()
7 | }
8 |
9 | dispatch({
10 | type: 'FETCH_TODOS_REQUEST',
11 | filter,
12 | })
13 |
14 | return new Promise((resolve, reject) => {
15 | asteroid.subscribe('Todos.list', filter)
16 | .on('ready', () => {
17 | dispatch({
18 | type: 'FETCH_TODOS_SUCCESS',
19 | filter,
20 | })
21 | })
22 | .on('error', error => {
23 | reject(error)
24 | dispatch({
25 | type: 'FETCH_TODOS_FAILURE',
26 | filter,
27 | message: error.message || 'Something went wrong',
28 | })
29 | })
30 | })
31 | }
32 |
33 | export const addTodo = (text) => (dispatch, getState, asteroid) => {
34 | // for optimistic UI we immediately dispatch an DDP_ADDED action
35 | const id = random.id()
36 | dispatch({
37 | type: 'DDP_ADDED',
38 | response: { collection: 'todos', doc: { id, text, completed: false } },
39 | })
40 | asteroid.call('addTodo', id, text).then(() => {
41 | // if this succeeds the todo has already been added
42 | // so there is nothing more todo
43 | })
44 | .catch(() => {
45 | // something went wrong when creating the new todo
46 | // since we optimistically added the todo already we need to remove it now
47 | dispatch({
48 | type: 'DDP_REMOVED',
49 | response: { collection: 'todos', id },
50 | })
51 | })
52 | }
53 |
54 | export const toggleTodo = (id) => (dispatch, getState, asteroid) => {
55 | const doc = getTodo(getState(), id)
56 | dispatch({
57 | type: 'DDP_CHANGED',
58 | response: { collection: 'todos', doc: { ...doc, completed: !doc.completed } },
59 | })
60 | asteroid.call('toggleTodo', id)
61 | .catch(() => {
62 | // something went wrong when creating the new todo
63 | // since we optimistically added the todo already we need to remove it now
64 | dispatch({
65 | type: 'DDP_CHANGED',
66 | response: { collection: 'todos', doc },
67 | })
68 | })
69 | }
70 |
--------------------------------------------------------------------------------
/client/src/components/todo-list.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react'
2 | import { connect } from 'react-redux'
3 | import { withRouter } from 'react-router'
4 | import Todo from './todo'
5 | import * as actions from '../actions'
6 | import { getVisibleTodos, getIsFetching, getErrorMessage } from '../reducers/'
7 | import FetchError from './fetch-error'
8 |
9 | class VisibileTodoList extends Component {
10 | componentDidMount() {
11 | this.fetchData()
12 | }
13 | componentDidUpdate(prevProps) {
14 | if (this.props.filter !== prevProps.filter) {
15 | this.fetchData()
16 | }
17 | }
18 | fetchData() {
19 | const { props: { filter, fetchTodos } } = this
20 | fetchTodos(filter)
21 | }
22 | render() {
23 | const { props: { isFetching, todos, toggleTodo, errorMessage } } = this
24 | if (isFetching && !todos.length) {
25 | return Loading...
26 | }
27 | if (errorMessage && !todos.length) {
28 | return ( this.fetchData()}
31 | />)
32 | }
33 | return
34 | }
35 | }
36 |
37 | VisibileTodoList.propTypes = {
38 | filter: PropTypes.string.isRequired,
39 | errorMessage: PropTypes.string,
40 | fetchTodos: PropTypes.func.isRequired,
41 | toggleTodo: PropTypes.func.isRequired,
42 | isFetching: PropTypes.bool.isRequired,
43 | todos: PropTypes.array.isRequired,
44 | }
45 |
46 | const TodoList = (props) => {
47 | const { todos, onToggleTodo } = props
48 | return (
49 |
50 | {todos.map(todo =>
51 |
52 | )}
53 |
54 | )
55 | }
56 |
57 | TodoList.propTypes = {
58 | todos: PropTypes.array.isRequired,
59 | onToggleTodo: PropTypes.func.isRequired,
60 | }
61 |
62 | const mapStateToProps = (state, { params }) => {
63 | const filter = params.filter || 'all'
64 | return {
65 | todos: getVisibleTodos(
66 | state,
67 | filter
68 | ),
69 | filter,
70 | isFetching: getIsFetching(state, filter),
71 | errorMessage: getErrorMessage(state, filter),
72 | }
73 | }
74 |
75 | export default withRouter(connect(
76 | mapStateToProps,
77 | actions
78 | )(VisibileTodoList))
79 |
--------------------------------------------------------------------------------
/client/src/reducers/createList.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import unique from 'lodash/uniq'
3 |
4 | const createList = (filter) => {
5 | const handleToggle = (state, action) => {
6 | const { response: { doc } } = action
7 | const { completed, id: toggleId } = doc
8 | const shouldRemove = (
9 | (completed && filter === 'active') ||
10 | (!completed && filter === 'completed')
11 | )
12 | return shouldRemove
13 | ? state.filter(id => id !== toggleId)
14 | // otherwise add if not added yet
15 | : unique([...state, toggleId])
16 | }
17 |
18 | const ids = (state = [], action) => {
19 | switch (action.type) {
20 | case 'DDP_ADDED':
21 | return (
22 | filter === 'all' ||
23 | (filter === 'active' && action.response.doc.completed === false) ||
24 | (filter === 'completed' && action.response.doc.completed === true)
25 | ) ? unique([...state, action.response.doc.id]) : state
26 | case 'DDP_REMOVED':
27 | return state.filter(id => id !== action.response.id)
28 | case 'DDP_CHANGED':
29 | return handleToggle(state, action)
30 | default:
31 | return state
32 | }
33 | }
34 |
35 | const isFetching = (state = false, action) => {
36 | if (action.filter !== filter) {
37 | return state
38 | }
39 | switch (action.type) {
40 | case 'FETCH_TODOS_REQUEST':
41 | return true
42 | case 'FETCH_TODOS_SUCCESS':
43 | case 'FETCH_TODOS_FAILURE':
44 | return false
45 | default:
46 | return state
47 | }
48 | }
49 |
50 | const errorMessage = (state = null, action) => {
51 | if (action.filter !== filter) {
52 | return state
53 | }
54 | switch (action.type) {
55 | case 'FETCH_TODOS_FAILURE':
56 | return action.message
57 | case 'FETCH_TODOS_REQUEST':
58 | case 'FETCH_TODOS_SUCCESS':
59 | return null
60 | default:
61 | return state
62 | }
63 | }
64 |
65 | return combineReducers({
66 | ids,
67 | isFetching,
68 | errorMessage,
69 | })
70 | }
71 |
72 | export default createList
73 |
74 | export const getIds = (state) => state.ids
75 | export const getIsFetching = (state) => state.isFetching
76 | export const getErrorMessage = (state) => state.errorMessage
77 |
--------------------------------------------------------------------------------