├── .gitignore ├── README.md ├── package.json ├── public ├── control-panel.html ├── index.html ├── main.css ├── message-board.html └── tasks.html ├── src ├── control-panel.js ├── flux │ ├── Dispatcher.js │ ├── ReduceStore.js │ ├── Store.js │ └── index.js ├── http │ └── index.js ├── message-board.js └── tasks.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional eslint cache 38 | .eslintcache 39 | 40 | # Optional REPL history 41 | .node_repl_history 42 | 43 | # Output of 'npm pack' 44 | *.tgz 45 | 46 | # Yarn Integrity file 47 | .yarn-integrity 48 | 49 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flux-Redux Exemplar 2 | 3 | ## Getting started 4 | To run application, run 5 | ```bash 6 | npm install 7 | ``` 8 | Optional, globally install dev dependencies (recommended for Windows) 9 | 10 | ```bash 11 | npm install -g webpack-dev-server webpack babel-preset-es2015 babel-plugin-transform-object-rest-spread babel-loader babel-core 12 | ``` 13 | Start the application 14 | ```bash 15 | npm start 16 | ``` 17 | 18 | Finally, navigate to `http://localhost:8080/` in your browser. 19 | 20 | ### Structure 21 | The application consists of three interconnected sub-applications with different implementations of Flux and Redux. 22 | 23 | #### Control Panel 24 | A basic form powered by a standards-compliant Flux implementation. 25 | 26 | #### Tasks 27 | A more advanced form based on the TodoMVC format, implementing a Flux ReduceStore. This form has `undo` functionality. 28 | 29 | #### Message Board 30 | A basic messaging tool similar to those found in most productivity apps. This sub-application implements Redux and contains asynchronous events. 31 | 32 | ### Glossary 33 | #### Flux https://github.com/facebook/flux 34 | Though available to download as a popular GitHub repository, Flux is actually simple enough to be implemented from scratch in under thirty minutes. In Flux, data is held in stores whose data cannot be changed by outside components. The store can change the data inside itself, and does so by listening to events emitted by the dispatcher. Flux also implements a ReduceStore, which has functionality that is highly similar to a Redux application. 35 | 36 | #### Facebook 37 | An American company best known for the popular social media application. Developers working at Facebook, or who form part of the React community, are credited with creating Flux, as well as authoring and maintaining the React and Redux libraries. It has made an above-average contribution to the open source community. 38 | #### Redux - https://github.com/reactjs/redux 39 | A library which is growing in popularity at an extreme pace, Redux expands on the ReduceStore first implemented in Flux. Redux is more sophisticated than Flux, and is generally considered first when creating new applications. However, numerous contributors, notably Dan Abramov (credited with the invention of Redux), make a point of advising developers to seek more basic alternatives first, before using Redux. 40 | 41 | #### Reducer 42 | An idempotent function, a reducer will always return the same output given the same arguments. Reducers usually take two arguments, an existing state and the action. If the action concerns the reducer in question (there can be many reducers in an application) it creates a copy of the state, modifies it accordingly, and returns that. Reducers, which never modify state, form the heart of all Redux applications. 43 | #### Store 44 | A structure for storing data, of which multiple are usually implemented for a Flux application, and just one for a Redux application. Usually, the data inside a store can be freely accessed but can not be changed from outside the store. In Flux, the store subscribes to actions which it then processes internally. After processing these actions, it emits a change letting other components access the new data. In a ReduceStore, and in Flux, stores must implement a function called a Reducer which never mutates state. 45 | 46 | ####Dispatcher 47 | Functioning as an unremarkable event dispatcher, this is the component in a Flux application which every other component can have access to. Actions are sent to the the dispatcher before flowing down to other components of the application. In Redux, the dispatcher is built in to the store. 48 | #### ReduceStore 49 | A class which extends the Flux Store and implements a reducer. It emits changes automatically based on whether the reducer changes the state, and supports immutability and undo functionality well. Redux works very similarly to a Flux application implementing a ReduceStore. 50 | #### React https://github.com/facebook/react 51 | A library for generating highly performant views created by some of the same team members as Flux. Though they are often taught and implemented together, React is just one possible view renderer that can be used with Redux or Flux (This project uses vanilla JavaScript as the view engine in order to remain unopinionated about the choice of view renderer). 52 | #### React-Redux 53 | A library containing helper functions which manage the repetitive tasks required to integrate React with Redux. As both React and Redux enforce unidirectional data flow and immutability, they integrate well together. Such applications tend to benefit both from the high-performance of React and the easy debugging of Redux. 54 | #### Angular-Redux 55 | Another library for connecting Redux to a common MVC framework. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flux-redux-exemplar", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "webpack-dev-server --content-base public/ --inline" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "redux": "^3.6.0", 15 | "redux-logger": "^2.7.4", 16 | "shortid": "^2.2.6" 17 | }, 18 | "engine": "node 7.2.0", 19 | "devDependencies": { 20 | "babel-core": "^6.18.2", 21 | "babel-loader": "^6.2.8", 22 | "babel-plugin-transform-object-rest-spread": "^6.19.0", 23 | "babel-preset-es2015": "^6.18.0", 24 | "webpack": "^1.13.3", 25 | "webpack-dev-server": "^1.16.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /public/control-panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Productivity App - Control Panel 6 | 7 | 8 | 9 | 10 | 24 |
25 |

Control Panel

26 |

27 | Welcome, Jim! 28 |

29 | 30 |
31 | 32 |
33 |
34 |

35 | Font size: 36 |

37 | Large
38 | Small
39 |
40 |
41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Productivity App - Tasks 6 | 7 | 8 | 9 | 10 | 11 | 12 | 26 |

Welcome to the Flux-Redux Productivity App!

27 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /public/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 5em; 3 | } 4 | section, input, button { 5 | margin-bottom: 0.6em; 6 | } -------------------------------------------------------------------------------- /public/message-board.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Productivity App - Message Board 6 | 7 | 8 | 9 | 10 | 24 |
25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 |
37 | 38 | 39 |
40 | 41 |
42 |
43 | 49 |
50 |
51 | 52 | -------------------------------------------------------------------------------- /public/tasks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Productivity App - Tasks 6 | 7 | 8 | 9 | 10 | 11 | 12 | 26 |

Tasks

27 |

28 | Welcome, Jim! 29 |

30 |
31 |
32 | 33 | 34 |
35 |
36 | 37 |
38 | 39 | 40 |
41 | 42 |
43 | 46 |
47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /src/control-panel.js: -------------------------------------------------------------------------------- 1 | import { Dispatcher, Store } from './flux'; 2 | 3 | const controlPanelDispatcher = new Dispatcher(); 4 | 5 | export const UPDATE_USERNAME = `UPDATE_USERNAME`; 6 | export const UPDATE_FONT_SIZE_PREFERENCE = `UPDATE_FONT_SIZE_PREFERENCE`; 7 | 8 | const userNameUpdateAction = (name)=>{ 9 | return { 10 | type: UPDATE_USERNAME, 11 | value: name 12 | } 13 | } 14 | 15 | const fontSizePreferenceUpdateAction = (size)=>{ 16 | return { 17 | type: UPDATE_FONT_SIZE_PREFERENCE, 18 | value: size 19 | } 20 | } 21 | // 22 | class UserPrefsStore extends Store { 23 | getInitialState() { 24 | return localStorage[`preferences`] ? JSON.parse(localStorage[`preferences`]) : { 25 | userName: "Jim", 26 | fontSize: "small" 27 | }; 28 | } 29 | __onDispatch(action){ 30 | switch(action.type) { 31 | case UPDATE_USERNAME: 32 | this.__state.userName = action.value; 33 | this.__emitChange(); 34 | break; 35 | case UPDATE_FONT_SIZE_PREFERENCE: 36 | this.__state.fontSize = action.value; 37 | this.__emitChange(); 38 | break; 39 | } 40 | } 41 | getUserPreferences(){ 42 | return this.__state; 43 | } 44 | } 45 | 46 | const userPrefsStore = new UserPrefsStore(controlPanelDispatcher); 47 | const userNameInput = document.getElementById(`userNameInput`); 48 | userNameInput.addEventListener("input",({target})=>{ 49 | const name = target.value; 50 | controlPanelDispatcher.dispatch(userNameUpdateAction(name)); 51 | }); 52 | 53 | const fontSizeForm = document.forms.fontSizeForm; 54 | 55 | fontSizeForm.fontSize.forEach(element=>{ 56 | element.addEventListener("change",({target})=>{ 57 | console.log("Buton change...",target.value); 58 | controlPanelDispatcher.dispatch(fontSizePreferenceUpdateAction(target.value)); 59 | }) 60 | }); 61 | 62 | const render = ({userName,fontSize})=>{ 63 | document.getElementById("userName").innerText = userName; 64 | document.getElementsByClassName("container")[0].style.fontSize = fontSize === "small" ? "16px" : "24px"; 65 | fontSizeForm.fontSize.value = fontSize; 66 | 67 | } 68 | 69 | userPrefsStore.addListener((state)=>{ 70 | render(state); 71 | saveUserPreferences(state); 72 | }); 73 | 74 | const saveUserPreferences =(state)=>{ 75 | localStorage[`preferences`] = JSON.stringify(state); 76 | } 77 | 78 | render(userPrefsStore.getUserPreferences()); -------------------------------------------------------------------------------- /src/flux/Dispatcher.js: -------------------------------------------------------------------------------- 1 | export class Dispatcher { 2 | constructor(){ 3 | this.__listeners = []; 4 | } 5 | dispatch(action){ 6 | this.__listeners.forEach(listener=>listener(action)); 7 | } 8 | register(listener){ 9 | this.__listeners.push(listener); 10 | } 11 | } -------------------------------------------------------------------------------- /src/flux/ReduceStore.js: -------------------------------------------------------------------------------- 1 | import {Store} from './Store'; 2 | export class ReduceStore extends Store { 3 | constructor(dispatcher){ 4 | super(dispatcher); 5 | this.__history = []; 6 | } 7 | reduce(state,action){ 8 | throw new Error("Subclasses must implement reduce method of a Flux ReduceStore"); 9 | } 10 | __onDispatch(action){ 11 | const newState = this.reduce(this.__state,action); 12 | if (newState !== this.__state) { 13 | this.__history.push(this.__state); 14 | this.__state = newState; 15 | this.__emitChange(); 16 | } 17 | } 18 | revertLastState(){ 19 | if (this.__history.length > 0) 20 | this.__state = this.__history.pop(); 21 | this.__emitChange(); 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/flux/Store.js: -------------------------------------------------------------------------------- 1 | export class Store { 2 | constructor(dispatcher){ 3 | this.__listeners = []; 4 | this.__state = this.getInitialState(); 5 | dispatcher.register(this.__onDispatch.bind(this)); 6 | } 7 | getInitialState(){ 8 | throw new Error("Subclasses must override getInitialState method of a Flux Store"); 9 | } 10 | __onDispatch(){ 11 | throw new Error("Subclasses must override __onDispatch method of a Flux Store"); 12 | } 13 | addListener(listener){ 14 | this.__listeners.push(listener); 15 | } 16 | __emitChange(){ 17 | this.__listeners.forEach(listener=>listener(this.__state)); 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /src/flux/index.js: -------------------------------------------------------------------------------- 1 | export {Dispatcher} from './Dispatcher'; 2 | export {Store} from './Store'; 3 | export {ReduceStore} from './ReduceStore'; 4 | -------------------------------------------------------------------------------- /src/http/index.js: -------------------------------------------------------------------------------- 1 | import { generate as id } from 'shortid'; 2 | const asyncAwaitTime = 500; 3 | export const get = (url, cb)=>{ 4 | setTimeout(()=>{ 5 | cb(id()); 6 | },asyncAwaitTime); 7 | } -------------------------------------------------------------------------------- /src/message-board.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, applyMiddleware } from 'redux' 2 | import { get } from './http'; 3 | import logger from 'redux-logger'; 4 | 5 | export const ONLINE = `ONLINE`; 6 | export const AWAY = `AWAY`; 7 | export const BUSY = `BUSY`; 8 | export const CREATE_NEW_MESSAGE = `CREATE_NEW_MESSAGE`; 9 | export const NEW_MESSAGE_SERVER_ACCEPTED = `NEW_MESSAGE_SERVER_ACCEPTED`; 10 | export const UPDATE_STATUS = `UPDATE_STATUS`; 11 | export const OFFLINE = `OFFLINE`; 12 | export const READY = `READY`; 13 | export const WAITING = `WAITING`; 14 | 15 | 16 | 17 | const defaultState = { 18 | messages:[{ 19 | date:new Date('2016-10-10 10:11:55'), 20 | postedBy:`Stan`, 21 | content:`I <3 the new productivity app!` 22 | },{ 23 | date:new Date('2016-10-10 10:12:00'), 24 | postedBy:`Jerry`, 25 | content:`I don't know if the new version of Bootstrap is really better...` 26 | },{ 27 | date:new Date('2016-10-10 12:06:04'), 28 | postedBy:`Llewlyn`, 29 | content:`Anyone got tickets to ng-conf?` 30 | }], 31 | userStatus: ONLINE, 32 | apiCommunicationStatus: READY 33 | } 34 | 35 | 36 | const newMessageAction = (content, postedBy, dispatch)=>{ 37 | const date = new Date(); 38 | 39 | // TODO... add asnychronicity to this action creator 40 | 41 | get('/api/create',(id=>{ 42 | store.dispatch({ 43 | type: NEW_MESSAGE_SERVER_ACCEPTED, 44 | value: content, 45 | postedBy, 46 | date, 47 | id 48 | }) 49 | })); 50 | 51 | return { 52 | type: CREATE_NEW_MESSAGE, 53 | value: content, 54 | postedBy, 55 | date 56 | } 57 | } 58 | 59 | const statusUpdateAction = (value)=>{ 60 | return { 61 | type: UPDATE_STATUS, 62 | value 63 | } 64 | } 65 | 66 | const userStatusReducer = (state = ONLINE, {type, value}) => { 67 | switch (type) { 68 | case UPDATE_STATUS: 69 | return value; 70 | } 71 | return state; 72 | } 73 | 74 | const apiCommunicationStatusReducer = (state = READY, {type}) => { 75 | switch (type) { 76 | case CREATE_NEW_MESSAGE: 77 | return WAITING; 78 | case NEW_MESSAGE_SERVER_ACCEPTED: 79 | return READY; 80 | } 81 | return state; 82 | } 83 | 84 | 85 | const messageReducer = (state = defaultState.messages, {type, value, postedBy, date}) => { 86 | switch (type) { 87 | case CREATE_NEW_MESSAGE: 88 | const newState = [ { date: date, postedBy, content: value } , ... state ] 89 | return newState; 90 | } 91 | return state; 92 | } 93 | 94 | const combinedReducer = combineReducers({ 95 | userStatus: userStatusReducer, 96 | messages: messageReducer, 97 | apiCommunicationStatus: apiCommunicationStatusReducer 98 | }) 99 | 100 | const store = createStore( 101 | combinedReducer, 102 | applyMiddleware(logger()) 103 | ); 104 | 105 | const render = ()=>{ 106 | const {messages, userStatus, apiCommunicationStatus} = store.getState(); 107 | document.getElementById("messages").innerHTML = messages 108 | .sort((a,b)=>b.date - a.date) 109 | .map(message=>(` 110 |
111 | ${message.postedBy} : ${message.content} 112 |
` 113 | )).join(""); 114 | 115 | document.forms.newMessage.newMessage.value = ""; 116 | document.forms.newMessage.fields.disabled = (userStatus === OFFLINE) || (apiCommunicationStatus === WAITING); 117 | 118 | } 119 | 120 | document.forms.newMessage.addEventListener("submit",(e)=>{ 121 | e.preventDefault(); 122 | const value = e.target.newMessage.value; 123 | const username = localStorage[`preferences`] ? JSON.parse(localStorage[`preferences`]).userName : "Jim"; 124 | store.dispatch(newMessageAction(value, username)); 125 | }); 126 | 127 | document.forms.selectStatus.status.addEventListener("change",(e)=>{ 128 | store.dispatch(statusUpdateAction(e.target.value)); 129 | }) 130 | 131 | render(); 132 | 133 | store.subscribe(render) -------------------------------------------------------------------------------- /src/tasks.js: -------------------------------------------------------------------------------- 1 | import { generate as id } from 'shortid'; 2 | import { Dispatcher, ReduceStore } from './flux'; 3 | 4 | const tasksDispatcher = new Dispatcher(); 5 | 6 | const CREATE_TASK = `CREATE_TASK`; 7 | const COMPLETE_TASK = `COMPLETE_TASK`; 8 | const SHOW_TASKS = `SHOW_TASKS`; 9 | 10 | const createNewTaskAction = (content)=>{ 11 | return { 12 | type: CREATE_TASK, 13 | value: content 14 | } 15 | } 16 | 17 | const completeTaskAction = (id,isComplete)=>{ 18 | return { 19 | type: COMPLETE_TASK, 20 | id, 21 | value: isComplete 22 | } 23 | } 24 | 25 | const showTasksAction = (show)=>{ 26 | return { 27 | type: SHOW_TASKS, 28 | value: show 29 | } 30 | } 31 | 32 | class TasksStore extends ReduceStore { 33 | getInitialState() { 34 | return { 35 | tasks: [{ 36 | id: id(), 37 | content: "Update CSS styles", 38 | complete: false 39 | }, { 40 | id: id(), 41 | content: "Add unit tests", 42 | complete: false 43 | }, { 44 | id: id(), 45 | content: "Post to social media", 46 | complete: false 47 | },{ 48 | id: id(), 49 | content: "Install hard drive", 50 | complete: true 51 | }], 52 | showComplete:true 53 | }; 54 | } 55 | reduce(state,action){ 56 | let newState; 57 | switch(action.type) { 58 | case CREATE_TASK: 59 | newState = { ...state, tasks: [ ... state.tasks ]}; 60 | newState.tasks.push({ 61 | id:id(), 62 | content:action.value, 63 | complete: false 64 | }) 65 | return newState; 66 | case COMPLETE_TASK: 67 | newState = { ... state, tasks: [ ... state.tasks ]}; 68 | const affectedElementIndex = newState.tasks.findIndex(t=>t.id === action.id); 69 | newState.tasks[affectedElementIndex] = { ... state.tasks[affectedElementIndex], complete: action.value } 70 | return newState; 71 | case SHOW_TASKS: 72 | newState = { ... state, showComplete: action.value }; 73 | return newState; 74 | } 75 | return state; 76 | } 77 | getState(){ 78 | return this.__state; 79 | } 80 | } 81 | 82 | const tasksStore = new TasksStore(tasksDispatcher); 83 | 84 | const TaskComponent = ({content,complete,id})=>( 85 | `
86 | ${content} 87 |
` 88 | ) 89 | 90 | const render = () => { 91 | const tasksSection = document.getElementById(`tasks`); 92 | const state = tasksStore.getState(); 93 | const rendered = tasksStore.getState().tasks 94 | .filter(task=>state.showComplete ? true : !task.complete) 95 | .map(TaskComponent).join(""); 96 | tasksSection.innerHTML = rendered; 97 | 98 | /* Add listeners to newly generated checkboxes */ 99 | document.getElementsByName('taskCompleteCheck').forEach(element=>{ 100 | element.addEventListener('change',(e)=>{ 101 | const id = e.target.attributes['data-taskid'].value; 102 | const checked= e.target.checked; 103 | tasksDispatcher.dispatch(completeTaskAction(id,checked)); 104 | }) 105 | }); 106 | 107 | if (localStorage[`preferences`]) { 108 | document.getElementById('userNameDisplay').innerHTML = JSON.parse(localStorage[`preferences`]).userName; 109 | } 110 | } 111 | 112 | document.forms.newTask.addEventListener('submit',(e)=>{ 113 | e.preventDefault(); 114 | const name = e.target.newTaskName.value; 115 | if (name) { 116 | tasksDispatcher.dispatch(createNewTaskAction(name)); 117 | e.target.newTaskName.value = null; 118 | } 119 | }) 120 | 121 | document.forms.undo.addEventListener('submit',(e)=>{ 122 | e.preventDefault(); 123 | tasksStore.revertLastState(); 124 | }) 125 | 126 | document.getElementById(`showComplete`).addEventListener('change',({target})=>{ 127 | const showComplete = target.checked; 128 | tasksDispatcher.dispatch(showTasksAction(showComplete)); 129 | }) 130 | 131 | tasksStore.addListener(()=>{ 132 | render(); 133 | }) 134 | 135 | render(); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | module.exports = { 3 | module: { 4 | loaders: [ 5 | { 6 | loader: "babel-loader", 7 | 8 | include: [ 9 | path.resolve(__dirname, "src"), 10 | ], 11 | 12 | test: /\.js?$/, 13 | 14 | // Options to configure babel with 15 | query: { 16 | presets: ['es2015'], 17 | plugins: ['transform-object-rest-spread'] 18 | } 19 | }, 20 | ] 21 | }, 22 | entry: { 23 | cpanel: ["./src/control-panel.js"], 24 | "message-board": ["./src/message-board.js"], 25 | tasks: ["./src/tasks.js"] 26 | }, 27 | output: { 28 | path: path.resolve(__dirname, "public"), 29 | publicPath: "/assets/", 30 | filename: "[name].bundle.js" 31 | }, 32 | devServer: { inline: true }, 33 | devtool: 'source-map', 34 | } --------------------------------------------------------------------------------