├── Procfile ├── routes ├── index │ ├── route.js │ ├── store.js │ ├── index.marko │ ├── components │ │ ├── app.marko │ │ └── counter.marko │ └── reducers │ │ └── index.js └── server-side-redux │ ├── index.marko │ ├── reducers │ └── index.js │ ├── route.js │ ├── store.js │ └── components │ ├── counter.marko │ └── app.marko ├── .gitignore ├── .jshintrc ├── package.json ├── server.js └── README.md /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start -------------------------------------------------------------------------------- /routes/index/route.js: -------------------------------------------------------------------------------- 1 | var view = require('./index'); 2 | 3 | module.exports = function(req, res) { 4 | res.marko(view, {}); 5 | }; -------------------------------------------------------------------------------- /routes/index/store.js: -------------------------------------------------------------------------------- 1 | var redux = require('redux'); 2 | var counter = require('./reducers'); 3 | 4 | module.exports = redux.createStore(counter); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.seed 2 | *.log 3 | *.csv 4 | *.dat 5 | *.out 6 | *.pid 7 | *.gz 8 | *.marko.js 9 | *.sublime-workspace 10 | *.sublime-project 11 | *.original 12 | 13 | /lib-cov 14 | /pids 15 | /logs 16 | /results 17 | /static 18 | /build 19 | /.cache 20 | /node_modules 21 | 22 | npm-debug.log -------------------------------------------------------------------------------- /routes/index/index.marko: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Marko + Redux 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /routes/server-side-redux/index.marko: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Marko + Redux 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /routes/index/components/app.marko: -------------------------------------------------------------------------------- 1 | import store from '../store'; 2 | 3 | class { 4 | onMount() { 5 | store.subscribe(() => { 6 | this.forceUpdate(); 7 | }); 8 | } 9 | 10 | dispatch(type) { 11 | store.dispatch({ type: type }); 12 | } 13 | } 14 | 15 |
16 | 19 |
-------------------------------------------------------------------------------- /routes/index/reducers/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function(state, action) { 2 | state = state || { 3 | value: 0 4 | }; 5 | 6 | switch (action.type) { 7 | case 'INCREMENT': 8 | return { 9 | value: state.value + 1 10 | }; 11 | case 'DECREMENT': 12 | return { 13 | value: state.value - 1 14 | }; 15 | default: 16 | return state; 17 | } 18 | }; -------------------------------------------------------------------------------- /routes/server-side-redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function(state, action) { 2 | state = state || { 3 | value: 0 4 | }; 5 | 6 | switch (action.type) { 7 | case 'INCREMENT': 8 | return { 9 | value: state.value + 1 10 | }; 11 | case 'DECREMENT': 12 | return { 13 | value: state.value - 1 14 | }; 15 | default: 16 | return state; 17 | } 18 | }; -------------------------------------------------------------------------------- /routes/server-side-redux/route.js: -------------------------------------------------------------------------------- 1 | var view = require('./index'); 2 | 3 | module.exports = function(req, res) { 4 | /* 5 | * We can create the initial state of the redux store on the server. 6 | * In this example, the initial state of our store is retrieved from 7 | * the request parameters. 8 | */ 9 | var value = parseInt(req.params.value); 10 | 11 | res.marko(view, { 12 | $global: { 13 | PRELOADED_STATE: { 14 | value: value, 15 | }, 16 | }, 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /routes/server-side-redux/store.js: -------------------------------------------------------------------------------- 1 | var redux = require('redux'); 2 | var counter = require('./reducers'); 3 | 4 | /* 5 | * We use an Immediately-Invoked Function Expression (IIFE) to hold our 6 | * redux store. This allows us to create a store client-side using the 7 | * preloaded state from the server. 8 | */ 9 | module.exports = (function() { 10 | var store; 11 | 12 | function initialize(preloadedState) { 13 | store = redux.createStore(counter, preloadedState); 14 | return store; 15 | } 16 | 17 | function getStore() { 18 | return store; 19 | } 20 | 21 | return { 22 | initialize: initialize, 23 | getStore: getStore, 24 | }; 25 | })(); 26 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node" : true, 3 | "esnext" : true, 4 | "browser" : true, 5 | "boss" : false, 6 | "curly": false, 7 | "debug": false, 8 | "devel": false, 9 | "eqeqeq": true, 10 | "evil": true, 11 | "forin": false, 12 | "immed": true, 13 | "laxbreak": false, 14 | "newcap": true, 15 | "noarg": true, 16 | "noempty": false, 17 | "nonew": true, 18 | "nomen": false, 19 | "onevar": false, 20 | "plusplus": false, 21 | "regexp": false, 22 | "undef": true, 23 | "sub": true, 24 | "white": false, 25 | "eqeqeq": false, 26 | "latedef": true, 27 | "unused": "vars", 28 | "strict": false, 29 | "eqnull": true 30 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "marko-redux", 3 | "version": "1.0.0", 4 | "description": "Sample app: Marko + Redux", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/marko-js-samples/marko-redux.git" 12 | }, 13 | "author": "Patrick Steele-idem ", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/marko-js-samples/marko-redux/issues" 17 | }, 18 | "homepage": "https://github.com/marko-js-samples/marko-redux", 19 | "dependencies": { 20 | "compression": "^1.6.2", 21 | "express": "^4.15.2", 22 | "lasso": "^2.11.5", 23 | "lasso-marko": "^2.3.0", 24 | "marko": "^4.0.0", 25 | "redux": "^3.6.0" 26 | }, 27 | "devDependencies": {} 28 | } 29 | -------------------------------------------------------------------------------- /routes/index/components/counter.marko: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a presentation component that has no knowledge of Redux. 3 | * When an action needs to be performed it simply emits an event 4 | * that needs to be handled by a parent container component. 5 | * In this example, the `app` component will handle the `increment` and 6 | * `decrement` actions by directly interacting with the Redux store. 7 | */ 8 | class { 9 | increment() { 10 | this.emit('increment'); 11 | } 12 | 13 | decrement() { 14 | this.emit('decrement'); 15 | } 16 | 17 | incrementIfOdd() { 18 | if (this.input.value % 2 !== 0) { 19 | this.increment(); 20 | } 21 | } 22 | 23 | incrementAsync() { 24 | setTimeout(() => { 25 | this.increment(); 26 | }, 1000); 27 | } 28 | } 29 | 30 |
31 | Current count: ${input.value} 32 |

33 | 34 | 35 | 38 | 41 |

42 |
-------------------------------------------------------------------------------- /routes/server-side-redux/components/counter.marko: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a presentation component that has no knowledge of Redux. 3 | * When an action needs to be performed it simply emits an event 4 | * that needs to be handled by a parent container component. 5 | * In this example, the `app` component will handle the `increment` and 6 | * `decrement` actions by directly interacting with the Redux store. 7 | */ 8 | class { 9 | increment() { 10 | this.emit('increment'); 11 | } 12 | 13 | decrement() { 14 | this.emit('decrement'); 15 | } 16 | 17 | incrementIfOdd() { 18 | if (this.input.value % 2 !== 0) { 19 | this.increment(); 20 | } 21 | } 22 | 23 | incrementAsync() { 24 | setTimeout(() => { 25 | this.increment(); 26 | }, 1000); 27 | } 28 | } 29 | 30 |
31 | Current count: ${input.value} 32 |

33 | 34 | 35 | 38 | 41 |

42 |
-------------------------------------------------------------------------------- /routes/server-side-redux/components/app.marko: -------------------------------------------------------------------------------- 1 | import store from '../store'; 2 | 3 | class { 4 | onCreate(input, out) { 5 | // out.global is received from the $global object from route.js 6 | this.PRELOADED_STATE = out.global.PRELOADED_STATE; 7 | 8 | // create redux store on the server-side 9 | this.reduxStore = store.initialize(this.PRELOADED_STATE); 10 | } 11 | 12 | onMount() { 13 | // recreate redux store on the client-side 14 | this.reduxStore = store.initialize(this.PRELOADED_STATE); 15 | this.reduxStore.subscribe(() => { 16 | this.forceUpdate(); 17 | }); 18 | } 19 | 20 | dispatch(type) { 21 | this.reduxStore.dispatch({ type: type }); 22 | } 23 | } 24 | 25 | $ var reduxState = {}; 26 | /* 27 | * On the client-side, Marko first does a "lightweight re-render" of the UI 28 | * component tree before the `onMount` event is fired. The DOM is not actually 29 | * updated in this "lightweight re-render". 30 | * We check for the existence of `this.reduxStore` because it is first created in 31 | * the `onMount` event. 32 | */ 33 | $ if(typeof component.reduxStore.getState === 'function') { 34 | reduxState = component.reduxStore.getState(); 35 | } 36 | 37 | 41 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require('marko/express'); 2 | require('marko/node-require'); 3 | 4 | var express = require('express'); 5 | var compression = require('compression'); // Provides gzip compression for the HTTP response 6 | 7 | var isProduction = process.env.NODE_ENV === 'production'; 8 | 9 | // Configure the RaptorJS Optimizer to control how JS/CSS/etc. is 10 | // delivered to the browser 11 | require('lasso').configure({ 12 | plugins: [ 13 | 'lasso-marko' // Allow Marko templates to be compiled and transported to the browser 14 | ], 15 | outputDir: __dirname + '/static', // Place all generated JS/CSS/etc. files into the "static" dir 16 | bundlingEnabled: isProduction, // Only enable bundling in production 17 | minify: isProduction, // Only minify JS and CSS code in production 18 | fingerprintsEnabled: isProduction, // Only add fingerprints to URLs in production 19 | }); 20 | 21 | 22 | var app = express(); 23 | 24 | var port = process.env.PORT || 8080; 25 | 26 | // Enable gzip compression for all HTTP responses 27 | app.use(compression()); 28 | 29 | // Allow all of the generated files under "static" to be served up by Express 30 | app.use(require('lasso/middleware').serveStatic()); 31 | 32 | // Map the "/" route to the home page 33 | app.get('/', require('./routes/index/route')); 34 | app.get('/server-side-redux/:value', require('./routes/server-side-redux/route')); 35 | 36 | app.listen(port, function(err) { 37 | if (err) { 38 | throw err; 39 | } 40 | console.log('Listening on port %d', port); 41 | 42 | // The browser-refresh module uses this event to know that the 43 | // process is ready to serve traffic after the restart 44 | if (process.send) { 45 | process.send('online'); 46 | } 47 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Marko + Redux 2 | ================================== 3 | 4 | This sample illustrates how to use Marko with Redux. Redux is a general-purpose application state container. This sample app illustrates how easy it is to wire up a Marko UI component with a Redux store. 5 | 6 | # Installation and running 7 | 8 | ```bash 9 | git clone https://github.com/marko-js-samples/marko-redux.git 10 | cd marko-redux 11 | npm install 12 | npm start 13 | ``` 14 | # More details 15 | 16 | The partial code snippet below shows how a a Marko UI component can be connected to a Redux store using the `store.subscribe()` method and the Marko `forceUpdate()` method: 17 | 18 | ```jsx 19 | import store from '../store'; 20 | 21 | class { 22 | onMount() { 23 | store.subscribe(() => { 24 | // Force this UI component to rerender: 25 | this.forceUpdate(); 26 | 27 | // The UI component will be rerendered using the new 28 | // state returned by `store.getState()` 29 | // 30 | // The following is another option to force an update: 31 | // this.input = store.getState(); 32 | }); 33 | } 34 | } 35 | 36 |
37 | 38 |
39 | ``` 40 | 41 | In the above example, the imported store module exports a Redux store created using the following code: 42 | 43 | ```js 44 | var redux = require('redux'); 45 | var counter = require('./reducers'); 46 | 47 | module.exports = redux.createStore(counter); 48 | ``` 49 | 50 | # Server-side rendering 51 | 52 | With Marko, we can also send the initial state of your Redux store from the server. 53 | 54 | We can pass the initial state as a `global` variable, as shown in the following code: 55 | 56 | ```js 57 | module.exports = function(req, res) { 58 | var value = parseInt(req.params.value); 59 | 60 | res.marko(view, { 61 | $global: { 62 | PRELOADED_STATE: { 63 | value: value, 64 | }, 65 | }, 66 | }); 67 | } 68 | ``` 69 | 70 | When we go to`localhost:8080/server-side/redux/123`, the `123` is used as the initial state of our redux store and passed to the client. 71 | 72 | In our Marko component, we can use the initial state like so: 73 | 74 | ```jsx 75 | import store from '../store'; 76 | 77 | class { 78 | onCreate(input, out) { 79 | // out.global is received from the $global object from route.js 80 | this.PRELOADED_STATE = out.global.PRELOADED_STATE; 81 | 82 | // create redux store on the server-side 83 | this.reduxStore = store.initialize(this.PRELOADED_STATE); 84 | } 85 | 86 | onMount() { 87 | // recreate redux store on the client-side 88 | this.reduxStore = store.initialize(this.PRELOADED_STATE); 89 | this.reduxStore.subscribe(() => { 90 | this.forceUpdate(); 91 | }); 92 | } 93 | 94 | dispatch(type) { 95 | this.reduxStore.dispatch({ type: type }); 96 | } 97 | } 98 | ``` 99 | 100 | In the above example, the imported store module is slightly different than the previous example. This module exports a Immediately-Invoked Function Expression (IFFE) which returns the Redux store. Using an IFFE allows us to create our Redux store with an initial state from the server, and share it with all other Marko components. 101 | --------------------------------------------------------------------------------