diff --git a/app/assets/javascripts/actions.js b/app/assets/javascripts/actions.js
index c38335c..e9b088c 100644
--- a/app/assets/javascripts/actions.js
+++ b/app/assets/javascripts/actions.js
@@ -2,24 +2,93 @@ import 'whatwg-fetch';
// TYPES
export const TODO_ITEMS_LOADING = 'TODO_ITEMS_LOADING';
+export const TODO_CREATE_ITEM = 'TODO_CREATE_ITEM';
+export const TODO_UPDATE_ITEM = 'TODO_UPDATE_ITEM';
+export const TODO_SOFT_UPDATE_ITEM = 'TODO_SOFT_UPDATE_ITEM';
+export const TODO_DELETE_ITEM = 'TODO_DELETE_ITEM';
export const TODO_ITEMS_RECEIVED = 'TODO_ITEMS_RECEIVED';
export const TODO_ITEMS_FAILURE = 'TODO_ITEMS_FAILURE';
// ACTION CREATORS
+export function load() {
+ return async (dispatch) => {
+ try {
+ dispatch({ type: TODO_ITEMS_LOADING });
+
+ const response = await fetch('/todo_items', {
+ method: 'GET',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ });
+ const data = await response.json();
+ dispatch({ type: TODO_ITEMS_RECEIVED, payload: { items: data } });
+ } catch (e) {
+ console.error(e);
+ }
+ };
+}
+
export function create(todo) {
return async (dispatch) => {
try {
- const response = await fetch(
- '/todo_items',
- {
- method: 'POST',
- body: JSON.stringify({ todo_item: todo }),
- headers: {
- Accept: 'application/json',
- 'Content-Type': 'application/json',
- },
+ dispatch({ type: TODO_CREATE_ITEM });
+
+ const response = await fetch('/todo_items', {
+ method: 'POST',
+ body: JSON.stringify({ todo_item: todo }),
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ });
+ const data = await response.json();
+ dispatch({ type: TODO_ITEMS_RECEIVED, payload: { items: data } });
+ } catch (e) {
+ console.error(e);
+ }
+ };
+}
+
+export function update(todo) {
+ return async (dispatch) => {
+ try {
+ dispatch({ type: TODO_UPDATE_ITEM });
+
+ const response = await fetch('/todo_items', {
+ method: 'PUT',
+ body: JSON.stringify({ todo_item: todo }),
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ });
+ const data = await response.json();
+ dispatch({ type: TODO_ITEMS_RECEIVED, payload: { items: data } });
+ } catch (e) {
+ console.error(e);
+ }
+ };
+}
+
+export function softUpdateItem(options) {
+ return { type: TODO_SOFT_UPDATE_ITEM, payload: options };
+}
+
+export function deleteItem(todo) {
+ return async (dispatch) => {
+ try {
+ dispatch({ type: TODO_DELETE_ITEM });
+
+ const response = await fetch('/todo_items', {
+ method: 'DELETE',
+ body: JSON.stringify({ todo_item: todo }),
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
},
- );
+ });
const data = await response.json();
dispatch({ type: TODO_ITEMS_RECEIVED, payload: { items: data } });
} catch (e) {
diff --git a/app/assets/javascripts/app.jsx b/app/assets/javascripts/app.jsx
index a45707d..732b898 100644
--- a/app/assets/javascripts/app.jsx
+++ b/app/assets/javascripts/app.jsx
@@ -3,7 +3,13 @@ import { render } from 'react-dom';
import PropTypes from 'prop-types';
import { connect, Provider } from 'react-redux';
import 'whatwg-fetch';
-import { create } from './actions';
+import {
+ load as loadItems,
+ create as createItem,
+ update as updateItem,
+ softUpdateItem,
+ deleteItem,
+} from './actions';
import configureStore from './configureStore';
// TODO:
@@ -16,7 +22,11 @@ import configureStore from './configureStore';
class App extends Component {
static propTypes = {
- create: PropTypes.func.isRequired,
+ createItem: PropTypes.func.isRequired,
+ loadItems: PropTypes.func.isRequired,
+ updateItem: PropTypes.func.isRequired,
+ softUpdateItem: PropTypes.func.isRequired,
+ deleteItem: PropTypes.func.isRequired,
newForm: PropTypes.shape({
text: PropTypes.string,
}),
@@ -33,44 +43,75 @@ class App extends Component {
super(props);
this.state = {
- data: [],
newForm: { ...this.props.newForm },
};
}
async componentWillMount() {
- const response = await fetch('/todo_items');
- const data = await response.json();
-
- this.setState({ data });
+ this.props.loadItems();
}
handleNewChange = (e) => {
this.setState({
newForm: { ...this.state.newForm, text: e.target.value },
});
- }
+ };
handleNewKeyUp = (e) => {
if (e.keyCode === 13) {
- this.props.create(this.state.newForm);
+ this.props.createItem(this.state.newForm);
this.setState({ newForm: { ...this.props.newForm } });
}
+ };
+
+ handleEditChange = idx => (e) => {
+ const modifiedItems = this.props.todos.items;
+ modifiedItems[idx] = {
+ ...modifiedItems[idx],
+ text: e.target.value,
+ };
+
+ this.props.softUpdateItem({ items: modifiedItems });
}
- render() {
- // TODO: Once the data is loaded from Redux completely, remove this
- const todos = this.props.todos.items.length ? this.props.todos.items : this.state.data;
+ handleEditKeyUp = idx => (e) => {
+ if (e.keyCode === 13) {
+ this.props.updateItem(this.props.todos.items[idx]);
+ }
+ };
+
+ itemChecked = idx => () => {
+ const item = this.props.todos.items[idx];
+ this.props.updateItem({
+ ...item,
+ is_done: !item.is_done,
+ });
+ }
+
+ deleteItem = idx => () => {
+ this.props.deleteItem(this.props.todos.items[idx]);
+ }
+ render() {
return (
To Do Is Cool
- {todos.map(item => (
+ {this.props.todos.items.map((item, idx) => (
-
-
-
-
+
+
+
))}
-
@@ -88,7 +129,13 @@ class App extends Component {
}
}
-const ConnectedApp = connect(state => ({ todos: state.todos }), { create })(App);
+const ConnectedApp = connect(state => ({ todos: state.todos }), {
+ loadItems,
+ createItem,
+ updateItem,
+ softUpdateItem,
+ deleteItem,
+})(App);
render(
diff --git a/app/assets/javascripts/reducer.js b/app/assets/javascripts/reducer.js
index 81230d8..c0bcf8f 100644
--- a/app/assets/javascripts/reducer.js
+++ b/app/assets/javascripts/reducer.js
@@ -2,9 +2,10 @@ import {
TODO_ITEMS_LOADING,
TODO_ITEMS_RECEIVED,
TODO_ITEMS_FAILURE,
+ TODO_SOFT_UPDATE_ITEM,
} from './actions';
-const initialState = {
+export const initialState = {
items: [],
};
@@ -17,6 +18,11 @@ export default function todosReducer(state = initialState, { type, payload }) {
...state,
items: payload.items,
};
+ case TODO_SOFT_UPDATE_ITEM:
+ return {
+ ...state,
+ items: payload.items,
+ };
case TODO_ITEMS_FAILURE:
return state;
default:
diff --git a/app/controllers/todo_items_controller.rb b/app/controllers/todo_items_controller.rb
index e088c5e..60c46c4 100644
--- a/app/controllers/todo_items_controller.rb
+++ b/app/controllers/todo_items_controller.rb
@@ -9,6 +9,20 @@ def create
index
end
+ def update
+ todo_item = TodoItem.find(params['todo_item']['id'])
+ todo_item.update(todo_item_params)
+
+ index
+ end
+
+ def destroy
+ todo_item = TodoItem.find(params['todo_item']['id'])
+ todo_item.destroy
+
+ index
+ end
+
private
def todo_item_params
diff --git a/config/routes.rb b/config/routes.rb
index 0accbd4..0b94909 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -2,6 +2,8 @@
scope 'todo_items' do
get '/' => 'todo_items#index'
post '/' => 'todo_items#create'
+ put '/' => 'todo_items#update'
+ delete '/' => 'todo_items#destroy'
end
root to: 'welcome#index'
diff --git a/spec/javascripts/actionsSpec.js b/spec/javascripts/actionsSpec.js
index 2d92b6e..335060a 100644
--- a/spec/javascripts/actionsSpec.js
+++ b/spec/javascripts/actionsSpec.js
@@ -1,9 +1,127 @@
-const expect = require('chai').expect;
+import configureMockStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+import {
+ load,
+ create,
+ update,
+ softUpdateItem,
+ deleteItem,
+ TODO_ITEMS_LOADING,
+ TODO_ITEMS_RECEIVED,
+ TODO_CREATE_ITEM,
+ TODO_UPDATE_ITEM,
+ TODO_SOFT_UPDATE_ITEM,
+ TODO_DELETE_ITEM,
+} from '../../app/assets/javascripts/actions';
+import fetchMock from 'fetch-mock';
+import { expect } from 'chai';
+
+const middlewares = [thunk];
+const mockStore = configureMockStore(middlewares);
+
+const TO_DO_1 = {
+ is_done: false,
+ text: 'TO_DO_1',
+ id: 1,
+};
+
+const TO_DO_2 = {
+ is_done: true,
+ text: 'TO_DO_2',
+ id: 2,
+};
// For a good idea of how Redux action creators are tested take a look at the
// documentation: https://redux.js.org/docs/recipes/WritingTests.html#action-creators
describe('Action Creators', () => {
- it('adds', () => {
- expect(1 + 1).to.equal(2);
+ afterEach(() => {
+ fetchMock.reset()
+ fetchMock.restore()
+ });
+
+ it('creates TODO_ITEMS_RECEIVED when fetching todos has been done', () => {
+ fetchMock
+ .getOnce('/todo_items', {
+ body: [TO_DO_1, TO_DO_2],
+ headers: { 'content-type': 'application/json' },
+ });
+
+ const expectedActions = [
+ { type: TODO_ITEMS_LOADING },
+ { type: TODO_ITEMS_RECEIVED, payload: { items: [TO_DO_1, TO_DO_2] } },
+ ];
+
+ const store = mockStore({ todos: { items: [] } });
+
+ return store.dispatch(load()).then(() => {
+ expect(store.getActions()).to.deep.equal(expectedActions);
+ });
});
+
+ it('creates TODO_CREATE_ITEM when create todo', () => {
+ fetchMock
+ .postOnce('/todo_items', {
+ body: [TO_DO_1, TO_DO_2],
+ headers: { 'content-type': 'application/json' },
+ });
+
+ const expectedActions = [
+ { type: TODO_CREATE_ITEM },
+ { type: TODO_ITEMS_RECEIVED, payload: { items: [TO_DO_1, TO_DO_2] } },
+ ];
+
+ const store = mockStore({ todos: { items: [] } });
+
+ return store.dispatch(create()).then(() => {
+ expect(store.getActions()).to.deep.equal(expectedActions)
+ });
+ });
+
+ it('creates TODO_UPDATE_ITEM when update todo', () => {
+ fetchMock
+ .putOnce('/todo_items', {
+ body: [TO_DO_1, TO_DO_2],
+ headers: { 'content-type': 'application/json' },
+ });
+
+ const expectedActions = [
+ { type: TODO_UPDATE_ITEM },
+ { type: TODO_ITEMS_RECEIVED, payload: { items: [TO_DO_1, TO_DO_2] } },
+ ];
+
+ const store = mockStore({ todos: { items: [] } });
+
+ return store.dispatch(update()).then(() => {
+ expect(store.getActions()).to.deep.equal(expectedActions)
+ });
+ });
+
+ it('creates TODO_SOFT_UPDATE_ITEM when update todo', () => {
+ const expectedAction =
+ { type: TODO_SOFT_UPDATE_ITEM, payload: { items: [TO_DO_2] } };
+
+ expect(softUpdateItem({ items: [TO_DO_2] })).to.deep.equal(expectedAction);
+ });
+
+
+ it('creates TODO_DELETE_ITEM when delete todo', () => {
+ fetchMock
+ .deleteOnce('/todo_items', {
+ body: [TO_DO_1, TO_DO_2],
+ headers: { 'content-type': 'application/json' },
+ });
+
+ const expectedActions = [
+ { type: TODO_DELETE_ITEM },
+ { type: TODO_ITEMS_RECEIVED, payload: { items: [TO_DO_1, TO_DO_2] } },
+ ];
+
+ const store = mockStore({ todos: { items: [] } });
+
+ return store.dispatch(deleteItem()).then(() => {
+ expect(store.getActions()).to.deep.equal(expectedActions)
+ });
+ });
+
+
});
diff --git a/spec/javascripts/reducerSpec.js b/spec/javascripts/reducerSpec.js
new file mode 100644
index 0000000..01e104f
--- /dev/null
+++ b/spec/javascripts/reducerSpec.js
@@ -0,0 +1,49 @@
+import reducer, { initialState } from '../../app/assets/javascripts/reducer';
+import { expect } from 'chai';
+
+import {
+ TODO_ITEMS_LOADING,
+ TODO_ITEMS_RECEIVED,
+ TODO_ITEMS_FAILURE,
+ TODO_SOFT_UPDATE_ITEM,
+} from '../../app/assets/javascripts/actions';
+
+const RECEIVED_ITEMS = 'RECEIVED_ITEMS';
+
+describe('todos reducer', () => {
+ it('should return the initial state', () => {
+ expect(reducer(undefined, {})).to.deep.equal(initialState);
+ });
+
+ it('should handle TODO_ITEMS_LOADING', () => {
+ expect(reducer(initialState, { type: TODO_ITEMS_LOADING })).to.deep.equal(initialState);
+ });
+
+ it('should handle TODO_ITEMS_RECEIVED', () => {
+ const action = {
+ type: TODO_ITEMS_RECEIVED,
+ payload: { items: RECEIVED_ITEMS },
+ };
+
+ expect(reducer(initialState, action)).to.deep.equal({
+ ...initialState,
+ items: RECEIVED_ITEMS,
+ });
+ });
+
+ it('should handle TODO_SOFT_UPDATE_ITEM', () => {
+ const action = {
+ type: TODO_SOFT_UPDATE_ITEM,
+ payload: { items: RECEIVED_ITEMS },
+ };
+
+ expect(reducer(initialState, action)).to.deep.equal({
+ ...initialState,
+ items: RECEIVED_ITEMS,
+ });
+ });
+
+ it('should handle TODO_ITEMS_FAILURE', () => {
+ expect(reducer(initialState, { type: TODO_ITEMS_FAILURE })).to.deep.equal(initialState);
+ });
+});