13 | Todo list without logs
14 | Todo list with logs
15 |
16 |
;
17 |
18 | return (
19 |
20 | {this.props.children || nav}
21 |
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom';
2 |
3 | import TodoActions from './flux-infra/actions/TodoActions';
4 | import routes from './routes/routes';
5 |
6 | // This action dispatches the READ_SUCCESS action to the store along with the data.
7 | // It then executes the callback passed as the second parameter.
8 | // Here, it renders the app and mounts it to the appropriate component.
9 | // Dispatching this action both client- and server-side is necessary as it
10 | // ensures that both will render the same markup because they have the same data.
11 | TodoActions.sendTodos(window.App, function(){
12 | ReactDOM.render(
13 | routes,
14 | document.getElementById('todoappcontainer')
15 | );
16 | });
17 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp'),
2 | eslint = require('gulp-eslint');
3 |
4 | gulp.task('lint', function () {
5 | // ESLint ignores files with "node_modules" paths.
6 | // So, it's best to have gulp ignore the directory as well.
7 | // Also, Be sure to return the stream from the task;
8 | // Otherwise, the task may end before the stream has finished.
9 | return gulp.src(['**/*.js','!node_modules/**'])
10 | // eslint() attaches the lint output to the "eslint" property
11 | // of the file object so it can be used by other modules.
12 | .pipe(eslint())
13 | // eslint.format() outputs the lint results to the console.
14 | // Alternatively use eslint.formatEach() (see Docs).
15 | .pipe(eslint.format())
16 | // To have the process exit with an error code (1) on
17 | // lint error, return the stream and pipe to failAfterError last.
18 | .pipe(eslint.failAfterError());
19 | });
20 |
--------------------------------------------------------------------------------
/routes/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Router, Route} from 'react-router/umd/ReactRouter';
3 | import createBrowserHistory from 'history/lib/createBrowserHistory';
4 | import createMemoryHistory from 'history/lib/createMemoryHistory';
5 |
6 | var history;
7 | if (typeof(window) !== 'undefined'){
8 | history = createBrowserHistory();
9 | }
10 | else {
11 | history = createMemoryHistory(); //This kind of history is needed for server-side rendering.
12 | }
13 |
14 | import TodoApp from '../components/TodoApp.react';
15 | import LogDiv from '../components/Logs.react';
16 | import AppContainer from '../components/AppContainer.react';
17 |
18 | var routes = (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | );
27 |
28 | export default routes;
29 |
--------------------------------------------------------------------------------
/components/Header.react.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TodoActions from '../flux-infra/actions/TodoActions';
3 | import TodoTextInput from './TodoTextInput.react';
4 |
5 | export default class Header extends React.Component{
6 |
7 | constructor(props){
8 | super(props);
9 | }
10 |
11 | /**
12 | * @return {object}
13 | */
14 | render () {
15 | return (
16 |
17 |
Universal todos
18 |
24 |
25 | );
26 | }
27 |
28 | /**
29 | * Event handler called within TodoTextInput.
30 | * Defining this here allows TodoTextInput to be used in multiple places
31 | * in different ways.
32 | * @param {string} text
33 | */
34 | _onSave (text) {
35 | if (text.trim()){
36 | TodoActions.create(text);
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Pierre Avizou
4 |
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 |
14 | The above copyright notice and this permission notice shall be included in
15 | all copies or substantial portions of the Software.
16 |
17 |
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | THE SOFTWARE.
25 |
--------------------------------------------------------------------------------
/flux-infra/stores/LogStore.js:
--------------------------------------------------------------------------------
1 | import AppDispatcher from '../dispatcher/AppDispatcher';
2 | import {EventEmitter} from 'events';
3 |
4 | var CHANGE_EVENT = 'change';
5 |
6 | var _logs = [];
7 |
8 | function addLog(text, item) {
9 | _logs.push(text + " : " + item);
10 | //console.log("log ajouté.");
11 | }
12 |
13 | function clearLogs(){
14 | console.log('Clearing logs');
15 | _logs = [];
16 | }
17 |
18 | class LogStore extends EventEmitter{
19 | constructor(){
20 | super();
21 | }
22 |
23 | getState(){
24 | return {
25 | logs: _logs
26 | };
27 | }
28 |
29 | emitChange(){
30 | this.emit(CHANGE_EVENT);
31 | }
32 |
33 | addChangeListener(callback){
34 | this.on(CHANGE_EVENT, callback);
35 | }
36 |
37 | removeChangeListener(callback){
38 | this.removeListener(CHANGE_EVENT, callback);
39 | }
40 | }
41 |
42 | var logStoreObj = new LogStore();
43 |
44 | AppDispatcher.register(function(payload){
45 | if (payload.actionType === 'LOG_DELETE'){
46 | clearLogs();
47 | logStoreObj.emitChange();
48 | return;
49 | }
50 | addLog(payload.actionType, payload.text || payload.id);
51 | logStoreObj.emitChange();
52 | //addLog(payload.actionType);
53 | });
54 |
55 | export default logStoreObj;
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "universal-routed-flux-demo",
3 | "version": "0.1.0",
4 | "description": "Get started building universal flux applications, with modern and exciting technologies such as Reactjs, React Router and es6.",
5 | "main": "entrypoint.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "node entrypoint.js"
9 | },
10 | "author": "Pierre Avizou",
11 | "license": "MIT",
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/pierreavizou/universal-routed-flux-demo.git"
15 | },
16 | "dependencies": {
17 | "babel-preset-es2015": "^6.3.13",
18 | "babel-preset-react": "^6.3.13",
19 | "babel-register": "^6.3.13",
20 | "babelify": "^7.2.0",
21 | "browserify-middleware": "^7.0.0",
22 | "classnames": "^2.1.5",
23 | "ejs": "^2.3.3",
24 | "express": "^4.13.3",
25 | "flux": "^2.1.1",
26 | "history": "^1.17.0",
27 | "keymirror": "^0.1.1",
28 | "react": "^0.14.3",
29 | "react-dom": "^0.14.3",
30 | "react-router": "^1.0.3",
31 | "socket.io": "^1.3.7",
32 | "socket.io-client": "^1.3.7"
33 | },
34 | "devDependencies": {
35 | "eslint": "^1.10.1",
36 | "eslint-plugin-react": "^3.10.0",
37 | "gulp": "^3.9.0",
38 | "gulp-eslint": "^1.1.1",
39 | "react-addons-perf": "^0.14.2"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/services/TodoStorageService.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | //simulate server delay
3 | const RESPONSE_TIME = 300;
4 |
5 | class TodoStorageService {
6 | constructor(storePath = __dirname + '/todos.json') {
7 | console.log('storage service initialised');
8 | this._todos = {};
9 | this.storePath = storePath;
10 | }
11 | write(todo, cb){
12 | this._todos[todo.id] = todo;
13 | setTimeout(() => {
14 | fs.writeFile(this.storePath, JSON.stringify(this._todos, null, 4), function(err){
15 | cb(err);
16 | });
17 | }, RESPONSE_TIME);
18 | }
19 |
20 | read(cb){
21 | fs.readFile(this.storePath, (err, data) => {
22 | setTimeout(() => {
23 | if (err !== null){
24 | cb(err, null);
25 | return;
26 | }
27 | this._todos = JSON.parse(data);
28 | cb(null, this._todos);
29 | }, RESPONSE_TIME);
30 | });
31 | }
32 |
33 | delete(id, cb){
34 | delete this._todos[id];
35 | setTimeout(() => {
36 | fs.writeFile(this.storePath, JSON.stringify(this._todos, null, 4), function(err){
37 | cb(err);
38 | });
39 | }, RESPONSE_TIME);
40 | }}
41 |
42 | export default new TodoStorageService();
43 |
--------------------------------------------------------------------------------
/components/MainSection.react.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TodoActions from '../flux-infra/actions/TodoActions';
3 | import TodoItem from './TodoItem.react';
4 | var ReactPropTypes = React.PropTypes;
5 |
6 | export default class MainSection extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | }
10 |
11 | render(){
12 | var allTodos = this.props.allTodos;
13 | // This section should be hidden by default
14 | // and shown when there are todos.
15 | if (Object.keys(allTodos).length < 1) {
16 | return null;
17 | }
18 |
19 | var todos = [];
20 |
21 | for (var key in allTodos) {
22 | todos.push();
23 | }
24 |
25 | return (
26 |
27 |
33 |
34 |
49 | );
50 | }
51 |
52 | _clearLogs(){
53 | LogActions.clearLogs();
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/components/Footer.react.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TodoActions from '../flux-infra/actions/TodoActions';
3 | var ReactPropTypes = React.PropTypes;
4 |
5 | export default class Footer extends React.Component {
6 | constructor(props) {
7 | super(props);
8 | }
9 |
10 | render(){
11 | var allTodos = this.props.allTodos;
12 | var total = Object.keys(allTodos).length;
13 |
14 | if (total === 0) {
15 | return null;
16 | }
17 |
18 | var completed = 0;
19 | for (var key in allTodos) {
20 | if (allTodos[key].complete) {
21 | completed++;
22 | }
23 | }
24 |
25 | var itemsLeft = total - completed;
26 | var itemsLeftPhrase = itemsLeft === 1 ? ' item ' : ' items ';
27 | itemsLeftPhrase += 'left';
28 |
29 | // Undefined and thus not rendered if no completed items are left.
30 | var clearCompletedButton;
31 | if (completed) {
32 | clearCompletedButton =
33 | ;
38 | }
39 |
40 | return (
41 |
50 | );
51 | }
52 |
53 | _onClearCompletedClick(){
54 | TodoActions.destroyCompleted();
55 | }
56 | }
57 |
58 | Footer.propTypes = {allTodos: ReactPropTypes.object.isRequired};
59 |
--------------------------------------------------------------------------------
/components/TodoApp.react.js:
--------------------------------------------------------------------------------
1 | import Footer from './Footer.react';
2 | import Header from './Header.react';
3 | import MainSection from './MainSection.react';
4 | import React from 'react';
5 | import TodoStore from '../flux-infra/stores/TodoStore';
6 | import TodoActions from '../flux-infra/actions/TodoActions';
7 | import {Link} from 'react-router';
8 |
9 | /**
10 | * Retrieve the current TODO data from the TodoStore
11 | */
12 | function getTodoState() {
13 | return {
14 | allTodos: TodoStore.getAll(),
15 | areAllComplete: TodoStore.areAllComplete()
16 | };
17 | }
18 |
19 | export default class TodoApp extends React.Component{
20 | constructor(props){
21 | super(props);
22 | this.state = getTodoState();
23 | this._onChange = this._onChange.bind(this);
24 | }
25 |
26 | componentDidMount(){
27 | TodoStore.addChangeListener(this._onChange);
28 | TodoActions.initSocket(); // We initialize the websocket connection as soon as the TodoApp component has mounted
29 | }
30 |
31 | componentWillUnmount(){
32 | TodoStore.removeChangeListener(this._onChange);
33 | }
34 |
35 | _onChange(){
36 | this.setState(getTodoState());
37 | }
38 |
39 | render(){
40 | return (
41 |
42 |
43 |
44 |
48 |
49 |
50 |
51 | {this.props.children || Display logs}
52 |
53 |
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/components/TodoTextInput.react.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | var ReactPropTypes = React.PropTypes;
4 | var ENTER_KEY_CODE = 13;
5 |
6 | export default class TodoTextInput extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {value : this.props.value} || '';
10 | this._save = this._save.bind(this);
11 | this._onChange = this._onChange.bind(this);
12 | this._onKeyDown = this._onKeyDown.bind(this);
13 | }
14 |
15 | render(){
16 | return (
17 |
27 | );
28 | }
29 |
30 | /**
31 | * Invokes the callback passed in as onSave, allowing this component to be
32 | * used in different ways.
33 | */
34 | _save() {
35 | this.props.onSave(this.state.value);
36 | this.setState({
37 | value: ''
38 | });
39 | }
40 |
41 | /**
42 | * @param {object} event
43 | */
44 | _onChange(/*object*/ event) {
45 | this.setState({
46 | value: event.target.value
47 | });
48 | }
49 |
50 | /**
51 | * @param {object} event
52 | */
53 | _onKeyDown(event) {
54 | if (event.keyCode === ENTER_KEY_CODE) {
55 | this._save();
56 | }
57 | }
58 | }
59 |
60 | TodoTextInput.propTypes = {
61 | className: ReactPropTypes.string,
62 | id: ReactPropTypes.string,
63 | placeholder: ReactPropTypes.string,
64 | onSave: ReactPropTypes.func.isRequired,
65 | value: ReactPropTypes.string
66 | };
67 |
68 | TodoTextInput.defaultProps = {saveOnBlur: true};
69 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Universal Flux example with React and React-Router
2 |
3 | **UPDATE 2** : *On the `immutable` branch you will find the app's TodoStore and LogStore implemented with Facebook's [Immutable](https://github.com/facebook/immutable-js) library. The implementation is inspired by [this one](https://github.com/MandarinConLaBarba/flux-immutable-todomvc).*
4 |
5 | **UPDATE** : *On the `flux-utils` branch you will find the flux architecture implemented with Flux's bundled `flux/utils`.*
6 |
7 | The code in this repo is intended for people who want to get started building universal flux applications, with modern and exciting technologies such as [React](https://facebook.github.io/react/), [React Router](https://github.com/rackt/react-router/) and [es6](https://github.com/ericdouglas/ES6-Learning).
8 |
9 | It consists of an example Todo Application built with flux and inspired by the [Facebook Flux tutorial](https://facebook.github.io/flux/docs/todo-list.html).
10 |
11 | The app has been rewritten to be fully universal (or ~~isomorphic~~), use es6, React Router, have a log component and provide asynchronous server communication.
12 |
13 | It combines the following notions in a full-featured example:
14 |
15 | * es6
16 | * Using React Router with server-side rendering
17 | * Flux architecture for universal applications
18 | * Multi-store application
19 | * Asynchronous server communication with visual feedback
20 |
21 | 
22 |
23 | ## Purpose
24 |
25 | This example app is here in the hope that it will help people getting started building great apps that use these recent technologies.
26 |
27 | After struggling to find guides and tutorials that combined everything I wanted to build an app, I decided to write and share an example of my own.
28 |
29 | ## Run the app
30 |
31 | The first thing you should do is clone the repo.
32 | Then, `cd`into the project folder and run `npm install`.
33 | This will download all the dependencies.
34 |
35 | Finally, all you have to do is run `node entrypoint.js` or `npm start`.
36 |
37 | ## Notes
38 |
39 | This app purposely uses no flux framework. The main reason is that frameworks usually hide many stuff from the user, which prevents from understanding properly how things work.
40 |
41 | Plus, the Flux architecture is reasonably clean, simple and elegant by itself, so using a framework is often not necessary, especially in example code.
42 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom/server';
4 | import browserify from 'browserify-middleware';
5 | import http from 'http';
6 | import TodoActions from './flux-infra/actions/TodoActions';
7 | import TodoStorageService from './services/TodoStorageService';
8 | import {match, RoutingContext} from 'react-router';
9 | import routes from './routes/routes';
10 | import initApi from './api/socketApi';
11 |
12 | var app = express();
13 | const HOST = process.env.HOST || 'http://localhost:3000';
14 |
15 | //browserify.settings.mode = 'production'; //uncomment to disable source watching and enable minification
16 | browserify.settings({transform: ['babelify']});
17 |
18 | var shared = ['react', 'react-dom', 'react-router/umd/ReactRouter', 'history'];
19 |
20 | // When a client requests the paths below, we serve a browserified and babelified version of the files.
21 | app.use('/build/shared.js', browserify(shared));
22 | app.use('/build/bundle.js', browserify('./app.js', {
23 | external: shared
24 | }));
25 |
26 | app.use(express.static(__dirname));
27 |
28 | app.get('*', function(req, res){
29 |
30 | match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
31 | if (error) {
32 | res.status(500).send(error.message);
33 | }
34 | else if (redirectLocation) {
35 | res.redirect(302, redirectLocation.pathname + redirectLocation.search);
36 | }
37 | else if (renderProps) {
38 |
39 | TodoStorageService.read(function(err, data){
40 | if(err !== null){
41 | console.log(err);
42 | res.send('Erreur : ' + err);
43 | res.end();
44 | return;
45 | }
46 | // This action dispatches the READ_SUCCESS action to the store
47 | // along with the data. It then executes the callback passed as
48 | // the second parameter. Here, it renders the app and sends the
49 | // HTML to the template.
50 | TodoActions.sendTodos(data, function(){
51 | var todoAppHtml = ReactDOM.renderToString();
52 | res.render('todo.ejs', {
53 | todoapp: todoAppHtml,
54 | todos: JSON.stringify(data),
55 | host: HOST
56 | });
57 | });
58 | });
59 | }
60 | else {
61 | res.status(404).send('Not found');
62 | }
63 | });
64 | });
65 |
66 | var server = http.createServer(app);
67 | server.listen(3000, function() {
68 | console.log('Listening on port 3000...');
69 | initApi(server, TodoStorageService);
70 | });
71 |
--------------------------------------------------------------------------------
/components/TodoItem.react.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TodoActions from '../flux-infra/actions/TodoActions';
3 | import TodoTextInput from './TodoTextInput.react';
4 | import classNames from 'classnames';
5 | var ReactPropTypes = React.PropTypes;
6 |
7 | export default class TodoItem extends React.Component {
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | isEditing: false
12 | };
13 | this._onToggleComplete = this._onToggleComplete.bind(this);
14 | this._onSave = this._onSave.bind(this);
15 | this._onDoubleClick = this._onDoubleClick.bind(this);
16 | this._onDestroyClick = this._onDestroyClick.bind(this);
17 | }
18 |
19 | render() {
20 | var todo = this.props.todo;
21 | var input;
22 | if (this.state.isEditing) {
23 | input =
24 | ;
29 | }
30 |
31 | // List items should get the class 'editing' when editing
32 | // and 'completed' when marked as completed.
33 | // Note that 'completed' is a classification while 'complete' is a state.
34 | // This differentiation between classification and state becomes important
35 | // in the naming of view actions toggleComplete() vs. destroyCompleted().
36 | return (
37 |