├── .gitignore ├── LICENSE ├── README.md ├── assets └── logo-header.png ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── publish.sh └── src ├── App.js ├── components ├── TodoFooter.js ├── TodoHeader.js ├── TodoItem.js ├── TodoList.js └── Todos.js ├── index.js ├── models └── todoModel.js └── services └── todoService.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 RxJS 中文社区 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | React RxJS Todos 3 |

4 | 5 | # React RxJS Todos 6 | 7 | This repo shows a basic todos example base on the famous [TodoMVC](https://github.com/tastejs/todomvc) but using RxJS and React. The goal is to show how to use the Observables data architecture pattern within React. The implementation was inspired by the [React Rx TodoMVC Example](https://github.com/fdecampredon/react-rxjs-todomvc). 8 | 9 | > Try the [live demo](https://rxjs-cn.github.io/react-rxjs-todos/) here. 10 | 11 | ## Install 12 | 13 | > This React app was build with [create-react-app](https://github.com/facebookincubator/create-react-app). 14 | 15 | ```shell 16 | # clone the repo 17 | git clone git@github.com:RxJS-CN/react-rxjs-todos.git 18 | 19 | # change into the repo directory 20 | cd react-rxjs-todos 21 | 22 | # install dependencies 23 | npm install 24 | 25 | # run 26 | npm start 27 | ``` 28 | 29 | Then visit [http://localhost:3000](http://localhost:3000) in your browser. 30 | 31 | ## Angular Version 32 | 33 | If you prefer Angular, you can checkout out [Angular RxJS Todos](https://github.com/RxJS-CN/angular-rxjs-todos) 34 | 35 | ## License 36 | 37 | MIT 38 | -------------------------------------------------------------------------------- /assets/logo-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RxJS-CN/react-rxjs-todos/b412b049b3e21081b45e3ccae6cc35009ddd28a7/assets/logo-header.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-rxjs-todos", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "classnames": "^2.2.5", 7 | "node-uuid": "^1.4.8", 8 | "react": "^15.6.1", 9 | "react-dom": "^15.6.1", 10 | "react-router-dom": "^4.1.2", 11 | "rxjs": "^5.4.2", 12 | "todomvc-app-css": "2.0.6", 13 | "todomvc-common": "1.0.2" 14 | }, 15 | "devDependencies": { 16 | "react-scripts": "1.0.10" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject", 23 | "publish": "./publish.sh" 24 | }, 25 | "homepage": "https://rxjs-cn.github.io/react-rxjs-todos/" 26 | } 27 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RxJS-CN/react-rxjs-todos/b412b049b3e21081b45e3ccae6cc35009ddd28a7/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React RxJS Todos 23 | 24 | 25 | 28 |
29 | 33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Generates production code and pushes it up to the site 4 | # 5 | 6 | rm -rf ./build && \ 7 | npm run build && \ 8 | git checkout gh-pages && \ 9 | git pull origin gh-pages && \ 10 | cp -r ./build/ ./ && \ 11 | rm -rf ./build && \ 12 | git add . && \ 13 | git commit -am "chore(publish): production code generated automatically" && \ 14 | git push origin gh-pages -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | BrowserRouter as Router, 4 | Route, 5 | Redirect 6 | } from 'react-router-dom'; 7 | 8 | import Todos from './components/Todos'; 9 | 10 | // todomvc styles 11 | import 'todomvc-common/base.css'; 12 | import 'todomvc-app-css/index.css'; 13 | 14 | 15 | const App = () => ( 16 |
17 | 18 | } /> 19 | 20 | 21 |
22 | 23 | 24 |
25 |
26 |
27 | ); 28 | 29 | export default App; -------------------------------------------------------------------------------- /src/components/TodoFooter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | const TodoFooter = ({ remainingCount, hasCompleted, onClearCompletedClick }) => { 5 | let clearCompletedButton; 6 | 7 | if (hasCompleted) { 8 | clearCompletedButton = ( 9 | 15 | ); 16 | } 17 | 18 | return ( 19 | 28 | ); 29 | }; 30 | 31 | export default TodoFooter; -------------------------------------------------------------------------------- /src/components/TodoHeader.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | 4 | const ENTER_KEY = 13; 5 | 6 | class TodoHeader extends Component { 7 | 8 | addTodo(event) { 9 | if (event.keyCode !== ENTER_KEY) { return ;} 10 | 11 | event.preventDefault(); 12 | 13 | const title = event.target.value.trim(); 14 | 15 | if (title) { 16 | this.props.onKeyDown(title); 17 | this.input.value = ''; 18 | } 19 | } 20 | 21 | render() { 22 | return ( 23 |
24 |

todos

25 | this.input = node} 30 | onKeyDown={this.addTodo.bind(this)} 31 | /> 32 |
33 | ) 34 | } 35 | }; 36 | 37 | export default TodoHeader; -------------------------------------------------------------------------------- /src/components/TodoItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import classnames from 'classnames'; 3 | 4 | const ENTER_KEY = 13; 5 | const ESCAPE_KEY = 27; 6 | 7 | class TodoItem extends Component { 8 | 9 | state = { 10 | editing: false, 11 | editTitle: this.props.todo.title 12 | } 13 | 14 | handleChange(event) { 15 | this.setState({ editTitle: event.target.value }); 16 | } 17 | 18 | handleEdit() { 19 | this.setState({ editing: true }); 20 | } 21 | 22 | handleKeyDown({ keyCode }) { 23 | if (keyCode === ESCAPE_KEY) { 24 | this.handleStop(); 25 | } else if (keyCode === ENTER_KEY) { 26 | this.handleStop(); 27 | } 28 | } 29 | 30 | handleStop() { 31 | this.setState({ editing: false }); 32 | this.handleSubmit(); 33 | } 34 | 35 | handleSubmit() { 36 | if (this.state.editTitle.trim().length) { 37 | this.props.onUpdate(this.props.todo.id, this.state.editTitle); 38 | } else { 39 | this.props.onRemoveClick(this.props.todo.id); 40 | } 41 | } 42 | 43 | render() { 44 | const { 45 | todo, 46 | onRemoveClick, 47 | onToggleClick, 48 | } = this.props; 49 | const { editing } = this.state; 50 | const liClass = classnames({ 51 | completed: todo.completed, 52 | editing : editing 53 | }); 54 | 55 | return ( 56 |
  • 57 |
    58 | onToggleClick(todo.id)} 63 | /> 64 | 65 | 70 |
    71 | 79 |
  • 80 | ) 81 | } 82 | } 83 | 84 | export default TodoItem; -------------------------------------------------------------------------------- /src/components/TodoList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import TodoItem from './TodoItem'; 4 | 5 | const TodoList = (props) => ( 6 |
    7 | 8 | 18 |
    19 | ) 20 | 21 | export default TodoList; -------------------------------------------------------------------------------- /src/components/Todos.js: -------------------------------------------------------------------------------- 1 | import React, { Component} from 'react'; 2 | 3 | import todoService from '../services/todoService'; 4 | 5 | import TodoHeader from './TodoHeader'; 6 | import TodoList from './TodoList'; 7 | import TodoFooter from './TodoFooter'; 8 | 9 | class Todos extends Component { 10 | 11 | state = { 12 | todos: [] 13 | } 14 | 15 | componentDidMount() { 16 | this.todos$ = todoService.todos$ 17 | .subscribe(todos => this.setState({ todos: todos })); 18 | } 19 | 20 | componentWillUnmount() { 21 | this.todos$.unsubscribe(); 22 | } 23 | 24 | getVisibleTodos() { 25 | const { filter } = this.props.match.params; 26 | const { todos } = this.state; 27 | 28 | switch (filter) { 29 | case 'active': 30 | return todos.filter(todo => !todo.completed); 31 | case 'completed': 32 | return todos.filter(todo => todo.completed); 33 | default: 34 | return todos; 35 | } 36 | } 37 | 38 | handleAdd(title) { 39 | todoService.add(title); 40 | } 41 | 42 | handleRemove(uuid) { 43 | todoService.remove(uuid); 44 | } 45 | 46 | handleRemoveCompleted() { 47 | todoService.removeCompleted(); 48 | } 49 | 50 | handleToggle(uuid) { 51 | todoService.toggle(uuid); 52 | } 53 | 54 | handleToggleAll(event) { 55 | todoService.toggleAll(event.target.checked); 56 | } 57 | 58 | handleUpdate(uuid, newTitle) { 59 | todoService.update(uuid, newTitle); 60 | } 61 | 62 | render() { 63 | const { todos } = this.state; 64 | let todoList, todoFooter; 65 | 66 | if (todos.length) { 67 | const remainingCount = todos.filter(todo => !todo.completed).length; 68 | const hasCompleted = todos.length > remainingCount; 69 | 70 | todoList = ( 71 | 78 | 79 | ); 80 | todoFooter = ( 81 | 86 | 87 | ); 88 | } 89 | 90 | return ( 91 |
    92 | 93 | {todoList} 94 | {todoFooter} 95 |
    96 | ) 97 | } 98 | } 99 | 100 | export default Todos; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); -------------------------------------------------------------------------------- /src/models/todoModel.js: -------------------------------------------------------------------------------- 1 | import { v4 } from 'node-uuid'; 2 | 3 | export default class Todo { 4 | 5 | constructor(title) { 6 | this.id = v4(); 7 | this.title = title; 8 | this.completed = false; 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /src/services/todoService.js: -------------------------------------------------------------------------------- 1 | import 'rxjs/Observable'; 2 | import { Subject } from 'rxjs/Subject'; 3 | import { BehaviorSubject } from 'rxjs/BehaviorSubject'; 4 | 5 | import 'rxjs/add/operator/scan'; 6 | import 'rxjs/add/operator/publishReplay'; 7 | import 'rxjs/add/operator/map'; 8 | 9 | import Todo from '../models/todoModel'; 10 | 11 | const initialTodos = JSON.parse(localStorage.getItem('react-rxjs-todos')) || []; 12 | 13 | class TodoService { 14 | 15 | constructor() { 16 | 17 | this.update$ = new BehaviorSubject(todos => todos); 18 | this.create$ = new Subject(); 19 | this.modify$ = new Subject(); 20 | this.remove$ = new Subject(); 21 | this.removeCompleted$ = new Subject(); 22 | this.toggle$ = new Subject(); 23 | this.toggleAll$ = new Subject(); 24 | 25 | 26 | this.createTodo$ = new Subject(); 27 | this.modifyTodo$ = new Subject(); 28 | this.removeTodo$ = new Subject(); 29 | this.removeCompletedTodos$ = new Subject(); 30 | this.toggleTodo$ = new Subject(); 31 | this.toggleAllTodos$ = new Subject(); 32 | 33 | 34 | this.todos$ = this.update$ 35 | .scan((todos, operation) => operation(todos), initialTodos) 36 | .publishReplay(1) 37 | .refCount(); 38 | 39 | this.todos$.forEach(todos => localStorage.setItem('react-rxjs-todos', JSON.stringify(todos))); 40 | 41 | this.create$ 42 | .map(todo => todos => todos.concat(todo)) 43 | .subscribe(this.update$); 44 | 45 | this.modify$ 46 | .map(({ uuid, newTitle }) => todos => { 47 | const targetTodo = todos.find(todo => todo.id === uuid); 48 | targetTodo.title = newTitle; 49 | return todos; 50 | }) 51 | .subscribe(this.update$); 52 | 53 | this.remove$ 54 | .map(uuid => todos => todos.filter(todo => todo.id !== uuid)) 55 | .subscribe(this.update$); 56 | 57 | this.removeCompleted$ 58 | .map(() => todos => todos.filter(todo => !todo.completed)) 59 | .subscribe(this.update$); 60 | 61 | this.toggle$ 62 | .map(uuid => todos => { 63 | const targetTodo = todos.find(todo => todo.id === uuid); 64 | targetTodo.completed = !targetTodo.completed; 65 | return todos; 66 | }) 67 | .subscribe(this.update$); 68 | 69 | this.toggleAll$ 70 | .map(completed => todos => { 71 | todos.forEach(todo => todo.completed = completed); 72 | return todos; 73 | }) 74 | .subscribe(this.update$); 75 | 76 | this.createTodo$ 77 | .subscribe(this.create$); 78 | 79 | this.modifyTodo$ 80 | .subscribe(this.modify$); 81 | 82 | this.removeTodo$ 83 | .subscribe(this.remove$); 84 | 85 | this.removeCompletedTodos$ 86 | .subscribe(this.removeCompleted$); 87 | 88 | this.toggleTodo$ 89 | .subscribe(this.toggle$); 90 | 91 | this.toggleAllTodos$ 92 | .subscribe(this.toggleAll$); 93 | 94 | } 95 | 96 | add(title) { 97 | this.createTodo$.next(new Todo(title)); 98 | } 99 | 100 | remove(uuid) { 101 | this.removeTodo$.next(uuid); 102 | } 103 | 104 | removeCompleted() { 105 | this.removeCompletedTodos$.next(); 106 | } 107 | 108 | toggle(uuid) { 109 | this.toggleTodo$.next(uuid); 110 | } 111 | 112 | toggleAll(completed) { 113 | this.toggleAllTodos$.next(completed); 114 | } 115 | 116 | update(uuid, newTitle) { 117 | this.modifyTodo$.next({ uuid, newTitle }); 118 | } 119 | 120 | } 121 | 122 | export default new TodoService(); --------------------------------------------------------------------------------