diff --git a/README.md b/README.md
index 168e577..e264e02 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,120 @@
# meteor-react-redux-example
This is a sample TODO app written in Meteor, React, and Redux. I start from tutorial app from Meteor and continue
-the integration from that, so most of the UI is from [here](https://www.meteor.com/tutorials/react/creating-an-app). I use this as a sandbox to explore the possibility to
-integrate these frameworks together.
+the integration from that, so most of the UI is from [here](https://www.meteor.com/tutorials/react/creating-an-app).
+I use this as a sandbox to explore the possibility to integrate these frameworks together.
+
+Please note that this is still a **WIP**.
# Goal
The goal of this exploration is to propose an integration approach that:
* Follow strictly [three principles](http://redux.js.org/docs/introduction/ThreePrinciples.html) from Redux.
- * Provide a clear guideline how to use Meteor API like Collection, Subscription, Method, etc within Redux.
+ * Provide a clear guideline how to use Meteor Collection, Subscription and Method.
* Embrace best practices from all frameworks.
+In general, we will place all side-effects call to redux actions using **react-thunk**. We try to avoid using unnecessary
+middleware or other libraries, unless it is really necessary.
+
+# Meteor Method
+
+Meteor Method can be considered as normal server-side call, and therefore is side-effect. We can wrap it inside an
+action and optionally dispatch call result.
+
+``` javascript
+export const removeAllTasks = () => (dispatch) => {
+ dispatch({ type: 'REMOVE_ALL_TASK_REQUEST' });
+ Meteor.call('removeAllTasks', (error, result) => {
+ if (err) dispatch({ type: 'REMOVE_ALL_TASK_ERROR', payload: error });
+ else dispatch({ type: 'REMOVE_ALL_TASK_SUCCESS', payload: result });
+ });
+};
+```
+
+## Collections
+
+### Subscription
+
+We can directly subscribe / unsubscribe to a subscription in an action.
+
+``` javascript
+export const someAction = (id) => (dispatch) => {
+ // ...
+ Meteor.subscribe('publicationName', { id });
+ // ...
+};
+```
+
+If we can determine if the lifecycle of a subscription is controlled by a component, we can enhance it with **meteorSubscribe**
+high-order component.
+
+``` javascript
+import meteorSubscribe from 'path/to/imports/lib/subscribe';
+
+export const enhancer = compose(
+ meteorSubscribe(props => ({ name: 'publicationName', options: { id: props.id } })),
+ otherEnhancer
+);
+
+export default enhancer(YourComponent);
+```
+
+When YourComponent is mounted, it will automatically subscribe to the publication and unsubscribe when the component is removed.
+
+### Fetching Data
+
+One of the coolest feature of Meteor is Reactive Collection. But since we don't use Blade we will have to manage the
+update by ourselves. The approach is similar to subscription, we use a Container component to manage the
+lifecycle of the cursor.
+
+``` javascript
+import cursorListener from 'path/to/imports/lib/cursorListener';
+
+class YourComponent extends React.Component {
+ // ...
+}
+
+export const enhancer = compose(
+ cursorListener(props => YourCollection.find({ someCondition: props.someValue }))
+);
+
+export default enhancer(YourComponent);
+```
+
+When YourComponent is mounted, it will listen to the cursor and start dispatching actions when there are changes. Then
+in your reducers, you can capture the change into store as below:
+
+```
+import { makeCursorReducer } from '../lib/cursorReducer';
+import Tasks from 'path/to/imports/api/yourCollection';
+import { combineReducers } from 'redux ;
+
+const tasks = makeCursorReducer(tasks);
+
+export default combineReducers({
+ // ....
+ tasks
+});
+```
+
+### Create/Update/Delete
+
+All CUD activities to Meteor collection are considered as side effect, because it changes the underlying storage. It is
+best to wrap all manipulation calls in actions, the change from the collection will propagate through cursor listener.
+There should be no Collection manipulation in reducers.
+
+
+``` javascript
+export const addTask = (text) => () => {
+ Tasks.insert({ text });
+};
+
+export const toggleTask = (id, checked) => () => {
+ Tasks.update({ _id: id }, { $set: { checked } });
+};
+```
+
# Data flow diagram

diff --git a/client/main.css b/client/main.css
index 3593c36..a4cdfa6 100644
--- a/client/main.css
+++ b/client/main.css
@@ -108,6 +108,12 @@ header .hide-completed {
.toggle-private {
margin-left: 5px;
}
+
+.remove-all-tasks {
+ text-align: right;
+ padding: 0.5em 15px;
+ cursor: pointer;
+}
@media (max-width: 600px) {
li {
diff --git a/imports/actionCreators.js b/imports/actionCreators.js
index 9f118c4..a02be79 100644
--- a/imports/actionCreators.js
+++ b/imports/actionCreators.js
@@ -2,30 +2,15 @@ import { Meteor } from 'meteor/meteor';
import { createAction } from 'redux-actions';
import Tasks from './api/tasks/collection';
-const createMeteorAction = (type, payloadTransfomer, meteorMeta) => (...args) => dispatch => {
- dispatch({
- type,
- payload: payloadTransfomer ? payloadTransfomer(...args) : args[0],
- meta: {
- meteor: meteorMeta
- }
- });
+export const addTask = (text) => () => {
+ Tasks.insert({ text });
};
-const createCollectionAction = (collection, type, payloadTransfomer) =>
- createMeteorAction(type, payloadTransfomer, {
- collection: typeof collection === 'object' ? collection._name : collection
- });
-
-export const ADD_TODO = 'ADD_TODO';
-export const addTodo = createCollectionAction(Tasks, ADD_TODO, (text) => ({ text }));
-
-export const REMOVE_TODO = 'REMOVE_TODO';
-export const removeTodo = createCollectionAction(Tasks, REMOVE_TODO);
+export const removeTodo = (id) => () => {
+ Tasks.remove({ _id: id });
+};
-export const TOGGLE_TODO = 'TOGGLE_TODO';
-export const toggleTodo = id => dispatch => {
- dispatch({ type: TOGGLE_TODO, payload: { id } });
+export const toggleTodo = id => () => {
const task = Tasks.findOne(id);
if (task) {
@@ -33,5 +18,16 @@ export const toggleTodo = id => dispatch => {
}
};
+// view state actions
export const TOGGLE_VISIBILITY_FILTER = 'TOGGLE_VISIBILITY_FILTER';
export const toggleVisibilityFilter = createAction(TOGGLE_VISIBILITY_FILTER);
+
+export const REMOVE_ALL_TASK_SUCCESS = 'REMOVE_ALL_TASK_SUCCESS';
+export const REMOVE_ALL_TASK_ERROR = 'REMOVE_ALL_TASK_ERROR';
+export const removeAllTasks = () => (dispatch) => {
+ dispatch({ type: 'REMOVE_ALL_TASK_REQUEST' });
+ Meteor.call('removeAllTasks', (error, result) => {
+ if (error) dispatch({ type: REMOVE_ALL_TASK_ERROR, payload: error });
+ else dispatch({ type: REMOVE_ALL_TASK_SUCCESS, payload: result });
+ });
+};
diff --git a/imports/api/tasks/server/index.js b/imports/api/tasks/server/index.js
index 43c7cb9..a641f47 100644
--- a/imports/api/tasks/server/index.js
+++ b/imports/api/tasks/server/index.js
@@ -1,3 +1,4 @@
+import { Meteor } from 'meteor/meteor';
import Tasks from '../collection';
import './publications';
@@ -7,3 +8,9 @@ Tasks.allow({
update: () => true,
remove: () => true
});
+
+Meteor.methods({
+ 'removeAllTasks'() {
+ return Tasks.remove({});
+ }
+});
\ No newline at end of file
diff --git a/imports/components/addtodo.js b/imports/components/addtodo.js
index 0567e2c..8bbc36b 100644
--- a/imports/components/addtodo.js
+++ b/imports/components/addtodo.js
@@ -1,12 +1,11 @@
import React from 'react';
import { compose } from 'recompose';
import { connect } from 'react-redux'
-import { addTodo } from '../actionCreators';
-
-class AddTodoForm extends React.Component {
+import { addTask } from '../actionCreators';
+class AddTaskForm extends React.Component {
handleSubmit(e) {
- this.props.addTodo(this.refs.textInput.value);
+ this.props.addTask(this.refs.textInput.value);
e.stopPropagation();
e.preventDefault();
@@ -27,8 +26,8 @@ class AddTodoForm extends React.Component {
const enhancer = compose(
connect(null, dispatch=> ({
- addTodo: (text) => dispatch(addTodo(text)),
+ addTask: (text) => dispatch(addTask(text)),
}))
);
-export default enhancer(AddTodoForm);
+export default enhancer(AddTaskForm);
diff --git a/imports/components/app.jsx b/imports/components/app.jsx
index d8520d9..5d5cb9f 100644
--- a/imports/components/app.jsx
+++ b/imports/components/app.jsx
@@ -1,13 +1,13 @@
import React, { Component } from 'react';
-import { compose } from 'recompose';;
+import { compose } from 'recompose';
import { connect } from 'react-redux'
-import { toggleVisibilityFilter } from '../actionCreators';
+import { toggleVisibilityFilter, removeAllTasks } from '../actionCreators';
import Tasks from '../api/tasks/collection';
import meteorSubscribe from '../lib/subscribe'
import cursorListener from '../lib/cursorListener';
import TaskList from './list';
-import AddTodoForm from './addtodo';
+import AddTaskForm from './addtodo';
class App extends Component {
renderTasks() {
@@ -30,10 +30,8 @@ class App extends Component {
/>
Hide Completed Tasks
-
-