├── LICENSE.md ├── README.md ├── custom-architecture-volune ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── README.md ├── api.md ├── assets │ └── loading.gif ├── index.html ├── package.json ├── src │ ├── app │ │ ├── component │ │ │ ├── ConnectedGifList.js │ │ │ ├── ConnectedGifPair.js │ │ │ ├── ConnectedGifViewer.js │ │ │ └── ConnectedPairOfGifPair.js │ │ ├── index.js │ │ ├── messages.js │ │ ├── reducer.js │ │ └── transformer.js │ ├── engine │ │ ├── createEngine.js │ │ ├── events.js │ │ ├── index.js │ │ ├── react │ │ │ ├── Provider.js │ │ │ ├── assemble.js │ │ │ ├── connect.js │ │ │ ├── helpers.js │ │ │ ├── index.js │ │ │ ├── propTypes.js │ │ │ └── toDispatchEventDictionary.js │ │ └── utils │ │ │ ├── index.js │ │ │ ├── toConsumer.js │ │ │ ├── toMapper.js │ │ │ ├── toMessages.js │ │ │ └── toTransformer.js │ └── modules │ │ ├── Button │ │ ├── component.js │ │ └── index.js │ │ ├── Counter │ │ ├── component.js │ │ └── index.js │ │ ├── GifList │ │ ├── ConnectedGifViewer │ │ │ └── index.js │ │ ├── component.js │ │ ├── consumer.js │ │ ├── index.js │ │ ├── mapper.js │ │ ├── messages.js │ │ ├── reducer.js │ │ └── transformer.js │ │ ├── GifPair │ │ ├── ConnectedGifViewer │ │ │ └── index.js │ │ ├── component.js │ │ ├── consumer.js │ │ ├── index.js │ │ ├── mapper.js │ │ └── messages.js │ │ ├── GifViewer │ │ ├── component.js │ │ ├── consumer.js │ │ ├── index.js │ │ ├── messages.js │ │ ├── reducer.js │ │ ├── service.js │ │ └── transformer.js │ │ └── PairOfGifPair │ │ ├── ConnectedGifPair │ │ └── index.js │ │ ├── component.js │ │ ├── consumer.js │ │ ├── index.js │ │ ├── mapper.js │ │ └── messages.js └── webpack.config.js ├── deku-redux-iazel ├── README.md ├── demo │ ├── bundle.js │ ├── index.html │ ├── loading.gif │ └── style.css ├── package.json ├── src │ ├── button │ │ ├── actions.js │ │ ├── component.js │ │ ├── model.js │ │ └── reducers.js │ ├── counter │ │ ├── actions.js │ │ ├── component.js │ │ ├── model.js │ │ └── reducers.js │ ├── deku-override │ │ ├── index.js │ │ └── render.js │ ├── index.js │ ├── list │ │ ├── actions.js │ │ ├── component.js │ │ ├── make.js │ │ ├── model.js │ │ └── reducers.js │ ├── main │ │ ├── actions.js │ │ ├── component.js │ │ ├── model.js │ │ └── reducers.js │ ├── middlewares │ │ ├── logger.js │ │ ├── observer.js │ │ └── task.js │ ├── pair │ │ ├── actions.js │ │ ├── component.js │ │ ├── make.js │ │ ├── model.js │ │ └── reducers.js │ ├── rndgif-list │ │ ├── component.js │ │ ├── model.js │ │ └── reducers.js │ ├── rndgif-pair-pair │ │ ├── component.js │ │ ├── model.js │ │ └── reducers.js │ ├── rndgif-pair │ │ ├── component.js │ │ ├── model.js │ │ └── reducers.js │ ├── rndgif │ │ ├── actions.js │ │ ├── api.js │ │ ├── component.js │ │ ├── model.js │ │ └── reducers.js │ ├── unique │ │ ├── actions.js │ │ ├── component.js │ │ ├── model.js │ │ └── reducers.js │ └── utils │ │ ├── actions.js │ │ ├── combine-reducers.js │ │ ├── events.js │ │ └── meta-reducers.js └── webpack.config.js ├── elm-api-extend ├── Button.elm ├── Counter.elm ├── Main.elm ├── README.md ├── RandomGif.elm ├── RandomGifPair.elm ├── RandomPairOfPair.elm └── elm-package.json ├── fractal-component ├── .babelrc.js ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── components │ │ ├── App.js │ │ ├── App.style.js │ │ ├── Counter │ │ │ ├── actions │ │ │ │ ├── index.js │ │ │ │ └── types.js │ │ │ ├── index.js │ │ │ └── styles │ │ │ │ └── index.js │ │ ├── RandomGif │ │ │ ├── actions │ │ │ │ ├── index.js │ │ │ │ └── types.js │ │ │ ├── index.js │ │ │ ├── reducers │ │ │ │ └── index.js │ │ │ ├── sagas │ │ │ │ └── index.js │ │ │ └── styles │ │ │ │ └── index.js │ │ ├── RandomGifPair │ │ │ ├── actions │ │ │ │ ├── index.js │ │ │ │ └── types.js │ │ │ ├── index.js │ │ │ ├── reducers │ │ │ │ └── index.js │ │ │ ├── sagas │ │ │ │ └── index.js │ │ │ └── styles │ │ │ │ └── index.js │ │ ├── RandomGifPairPair │ │ │ ├── actions │ │ │ │ ├── index.js │ │ │ │ └── types.js │ │ │ ├── index.js │ │ │ ├── reducers │ │ │ │ └── index.js │ │ │ ├── sagas │ │ │ │ └── index.js │ │ │ └── styles │ │ │ │ └── index.js │ │ └── ToggleButton │ │ │ ├── actions │ │ │ ├── index.js │ │ │ └── types.js │ │ │ ├── index.js │ │ │ └── styles │ │ │ └── index.js │ └── main.js └── webpack.config.js ├── localprovider-redux-saga-ganaraj ├── .babelrc ├── .gitignore ├── README.md ├── package.json ├── src │ ├── LocalProvider.js │ ├── counter │ │ ├── Container.js │ │ ├── actions.js │ │ ├── index.js │ │ └── reducer.js │ ├── index.js │ ├── main │ │ └── index.js │ ├── randomGif │ │ ├── Container.js │ │ ├── actions.js │ │ ├── gifEndpoint.js │ │ ├── index.js │ │ ├── reducer.js │ │ └── saga.js │ ├── randomGifList │ │ ├── Container.js │ │ ├── actions.js │ │ ├── index.js │ │ └── reducer.js │ ├── randomGifPair │ │ ├── Container.js │ │ └── index.js │ ├── randomGifPairOfPair │ │ ├── Container.js │ │ └── index.js │ ├── saga.js │ ├── styles.css │ └── theButton │ │ ├── Container.js │ │ ├── actions.js │ │ ├── index.js │ │ └── reducer.js └── webpack.config.js ├── modux-js-pcreations ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ └── index.html └── src │ ├── App.css │ ├── App.js │ ├── DevTools.js │ ├── button │ └── index.js │ ├── configureStore.js │ ├── counter │ └── index.js │ ├── gif-viewer-list │ └── index.js │ ├── gif-viewer-pair-pair │ └── index.js │ ├── gif-viewer-pair │ └── index.js │ ├── gif-viewer │ ├── effects.js │ └── index.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── newGifCounterAndButton.js │ ├── root.js │ └── sagaMonitor.js ├── react-opaque-components-bodhi ├── README.md ├── index.html ├── package.json ├── server.js ├── src │ ├── App.js │ ├── Button.js │ ├── Counter.js │ ├── Gif.js │ └── index.js └── webpack.config.js ├── redux+petux-tempname11 ├── .flowconfig ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ └── index.html └── src │ ├── App │ ├── Component.js │ ├── reducer.js │ └── types.js │ ├── Button │ ├── Component.js │ ├── reducer.js │ └── types.js │ ├── Counter │ ├── Component.js │ ├── reducer.js │ └── types.js │ ├── Pair │ ├── Component.js │ ├── reducer.js │ └── types.js │ ├── RandomGif │ ├── Component.js │ ├── reducer.js │ └── types.js │ ├── RoutedEffect.js │ ├── effects.js │ ├── index.js │ └── local-effects.js ├── redux-architecture-jarvisaoieong ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmrc ├── README.md ├── package.json ├── server.js ├── src │ ├── helpers │ │ ├── createStore.js │ │ └── superagent.js │ ├── index.html │ ├── index.js │ └── modules │ │ ├── button │ │ ├── actions.js │ │ ├── components │ │ │ └── Button.js │ │ ├── index.js │ │ ├── init.js │ │ └── reducer.js │ │ ├── counter │ │ ├── actions.js │ │ ├── components │ │ │ └── Counter.js │ │ ├── index.js │ │ ├── init.js │ │ └── reducer.js │ │ ├── main │ │ ├── actions.js │ │ ├── components │ │ │ ├── ButtonContainer.js │ │ │ ├── CounterContainer.js │ │ │ ├── Main.js │ │ │ ├── RandomGifContainer.js │ │ │ ├── RandomGifListContainer.js │ │ │ ├── RandomGifPairContainer.js │ │ │ └── RandomGifPairOfPairContainer.js │ │ ├── index.js │ │ ├── init.js │ │ ├── newGifCountHor.js │ │ └── reducer.js │ │ ├── randomGif │ │ ├── actions.js │ │ ├── components │ │ │ ├── RandomGif.js │ │ │ └── waiting.gif │ │ ├── index.js │ │ ├── init.js │ │ ├── reducer.js │ │ └── tasks.js │ │ ├── randomGifList │ │ ├── actions.js │ │ ├── components │ │ │ └── RandomGifList.js │ │ ├── index.js │ │ ├── init.js │ │ └── reducer.js │ │ ├── randomGifPair │ │ ├── actions.js │ │ ├── components │ │ │ └── RandomGifPair.js │ │ ├── index.js │ │ ├── init.js │ │ └── reducer.js │ │ └── randomGifPairOfPair │ │ ├── actions.js │ │ ├── components │ │ └── RandomGifPairOfPair.js │ │ ├── index.js │ │ ├── init.js │ │ └── reducer.js ├── webpack.config.js ├── webpack.development.js └── webpack.production.js ├── redux-elm-tomkis1 ├── .babelrc ├── .gitignore ├── assets │ └── waiting.gif ├── index.html ├── package.json ├── src │ ├── boilerplate.js │ ├── button │ │ ├── updater.js │ │ └── view.js │ ├── counter │ │ ├── updater.js │ │ └── view.js │ ├── gif-viewer-pair-of-pairs │ │ ├── updater.js │ │ └── view.js │ ├── gif-viewer-pair │ │ ├── updater.js │ │ └── view.js │ ├── gif-viewer │ │ ├── effects.js │ │ ├── updater.js │ │ └── view.js │ ├── main.js │ └── root │ │ ├── updater.js │ │ └── view.js └── webpack.config.js ├── redux-fly-mrefrem ├── .editorconfig ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ └── index.html └── src │ ├── App.js │ ├── Button.js │ ├── Counter.js │ ├── PairGif.js │ ├── PairOfPairGif.js │ ├── RandomGif.js │ └── index.js ├── redux-operations-DrorT ├── .babelrc ├── .gitignore ├── DevTools.js ├── README.md ├── components │ ├── Button.js │ ├── Counter.js │ ├── Counter2.js │ ├── PairOfRandomGifs.js │ └── RandomGif.js ├── configureStore.js ├── containers │ └── App.js ├── ducks │ ├── button.js │ ├── counter.js │ ├── counter2.js │ ├── gifCounter.js │ └── randomGif.js ├── index.html ├── index.js ├── package.json ├── rootReducer.js ├── server.js └── webpack.config.js ├── redux-saga-jaysoo ├── .babelrc ├── .gitignore ├── README.md ├── build │ └── index.html ├── package.json ├── src │ ├── counter │ │ ├── Container.js │ │ ├── __init__.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── model.js │ │ ├── reducer.js │ │ └── selectors.js │ ├── index.js │ ├── main │ │ └── index.js │ ├── randomGif │ │ ├── Container.js │ │ ├── __init__.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── model.js │ │ ├── reducer.js │ │ ├── saga.js │ │ ├── selectors.js │ │ └── tasks.js │ ├── randomGifList │ │ ├── Container.js │ │ ├── __init__.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── reducer.js │ │ ├── saga.js │ │ └── selectors.js │ ├── randomGifPair │ │ ├── Container.js │ │ ├── __init__.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── reducer.js │ │ ├── saga.js │ │ └── selectors.js │ ├── randomGifPairOfPair │ │ ├── Container.js │ │ ├── __init__.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── reducer.js │ │ ├── saga.js │ │ └── selectors.js │ ├── styles.css │ ├── tasks │ │ ├── actions.js │ │ ├── index.js │ │ └── saga.js │ ├── theButton │ │ ├── Container.js │ │ ├── __init__.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── model.js │ │ ├── reducer.js │ │ └── selectors.js │ └── utils.js └── webpack.config.js ├── redux-serial-effects ├── .gitignore ├── README.md ├── config │ ├── env.js │ ├── paths.js │ ├── polyfills.js │ ├── webpack.config.dev.js │ └── webpackDevServer.config.js ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── scripts │ └── start.js └── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── button │ ├── Container.js │ ├── actionTypes.js │ ├── actions.js │ └── reducer.js │ ├── counter │ ├── Container.js │ ├── actionTypes.js │ ├── actions.js │ └── reducer.js │ ├── encapsulateComponent.js │ ├── gif │ ├── Container.js │ ├── actionTypes.js │ ├── actions.js │ ├── reducer.js │ ├── states.js │ └── subscriber.js │ ├── index.css │ ├── index.js │ ├── main │ ├── Container.js │ ├── mountPoints.js │ ├── rootReducer.js │ └── rootSubscriber.js │ ├── randomGifPair │ ├── Container.js │ ├── reducer.js │ └── subscriber.js │ ├── randomGifPairOfPair │ ├── Container.js │ ├── reducer.js │ └── subscriber.js │ ├── registerServiceWorker.js │ └── relocatableGif │ ├── Container.js │ ├── reducer.js │ └── subscriber.js ├── redux-ship-clarus ├── .flowconfig ├── .gitignore ├── README.md ├── decls │ └── jest.js ├── package.json ├── public │ ├── favicon.ico │ └── index.html └── src │ ├── __tests__ │ ├── __snapshots__ │ │ ├── model.test.js.snap │ │ └── view.test.js.snap │ ├── controller.test.js │ ├── model.test.js │ └── view.test.js │ ├── button │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── model.test.js.snap │ │ │ └── view.test.js.snap │ │ ├── model.test.js │ │ └── view.test.js │ ├── model.js │ └── view.js │ ├── controller.js │ ├── counter │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── model.test.js.snap │ │ │ └── view.test.js.snap │ │ ├── model.test.js │ │ └── view.test.js │ ├── model.js │ └── view.js │ ├── effect.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── model.js │ ├── random-gif-pair-pair │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── model.test.js.snap │ │ │ └── view.test.js.snap │ │ ├── model.test.js │ │ └── view.test.js │ ├── controller.js │ ├── model.js │ └── view.js │ ├── random-gif-pair │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── model.test.js.snap │ │ │ └── view.test.js.snap │ │ ├── model.test.js │ │ └── view.test.js │ ├── controller.js │ ├── model.js │ ├── view.css │ └── view.js │ ├── random-gif │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── controller.test.js.snap │ │ │ ├── model.test.js.snap │ │ │ └── view.test.js.snap │ │ ├── controller.test.js │ │ ├── model.test.js │ │ └── view.test.js │ ├── controller.js │ ├── model.js │ ├── view.css │ └── view.js │ ├── store.js │ ├── test.js │ ├── view.css │ └── view.js ├── redux-subspace-mpeyper ├── README.md ├── package.json ├── public │ └── index.html └── src │ ├── api.js │ ├── app │ ├── App.js │ ├── index.js │ └── reducer.js │ ├── button │ ├── Button.js │ ├── actions.js │ ├── index.js │ └── reducer.js │ ├── counter │ ├── Counter.js │ ├── actions.js │ ├── index.js │ └── reducer.js │ ├── index.js │ ├── randomGif │ ├── RandomGif.js │ ├── actions.js │ ├── index.js │ └── reducer.js │ ├── randomGifPair │ ├── RandomGifPair.js │ ├── index.js │ └── reducer.js │ ├── randomGifPairPair │ ├── RandomGifPairPair.js │ ├── index.js │ └── reducer.js │ └── store.js ├── tom-binary-tree ├── .gitignore ├── README.md ├── dist │ └── index.html ├── package.json ├── src │ ├── Button.js │ ├── Counter.js │ ├── Main.js │ ├── RandomGif.js │ ├── RandomGifPair.js │ ├── RandomGifPairOfPair.js │ ├── compose.js │ └── index.js └── webpack.config.js └── upward-message ├── Button.elm ├── Counter.elm ├── Main.elm ├── README.md ├── RandomGif.elm ├── RandomGifPair.elm ├── RandomPairOfPair.elm └── elm-package.json /custom-architecture-volune/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "plugins": [ 4 | "transform-function-bind", 5 | "transform-class-properties", 6 | "transform-export-extensions", 7 | "syntax-trailing-function-commas", 8 | "transform-object-rest-spread" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /custom-architecture-volune/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /custom-architecture-volune/.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | -------------------------------------------------------------------------------- /custom-architecture-volune/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true 5 | }, 6 | "parser": "babel-eslint", 7 | "extends": "airbnb", 8 | "settings": { 9 | "import/resolver": "webpack" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /custom-architecture-volune/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | -------------------------------------------------------------------------------- /custom-architecture-volune/assets/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slorber/scalable-frontend-with-elm-or-redux/f42646d2fc56949420eb802a85d851b0383f0069/custom-architecture-volune/assets/loading.gif -------------------------------------------------------------------------------- /custom-architecture-volune/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | custom-architecture 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/app/component/ConnectedGifList.js: -------------------------------------------------------------------------------- 1 | import { connect, toDispatchEventDictionary } from 'engine/react'; 2 | import GifList from 'modules/GifList'; 3 | import Msg from '../messages'; 4 | 5 | const mapEventsToProps = { 6 | onNewGif: Msg.GIF_RECEIVED, 7 | }::toDispatchEventDictionary(); 8 | 9 | export default connect({ 10 | mapEventsToProps, 11 | })(GifList); 12 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/app/component/ConnectedGifPair.js: -------------------------------------------------------------------------------- 1 | import { connect, toDispatchEventDictionary } from 'engine/react'; 2 | import GifPair from 'modules/GifPair'; 3 | import Msg from '../messages'; 4 | 5 | const mapEventsToProps = { 6 | onNewGif: Msg.GIF_RECEIVED, 7 | }::toDispatchEventDictionary(); 8 | 9 | export default connect({ 10 | mapEventsToProps, 11 | })(GifPair); 12 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/app/component/ConnectedGifViewer.js: -------------------------------------------------------------------------------- 1 | import { connect, toDispatchEventDictionary } from 'engine/react'; 2 | import GifViewer from 'modules/GifViewer'; 3 | import Msg from '../messages'; 4 | 5 | const mapEventsToProps = { 6 | onNewGif: Msg.GIF_RECEIVED, 7 | }::toDispatchEventDictionary(); 8 | 9 | export default connect({ 10 | mapEventsToProps, 11 | })(GifViewer); 12 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/app/component/ConnectedPairOfGifPair.js: -------------------------------------------------------------------------------- 1 | import { connect, toDispatchEventDictionary } from 'engine/react'; 2 | import PairOfGifPair from 'modules/PairOfGifPair'; 3 | import Msg from '../messages'; 4 | 5 | const mapEventsToProps = { 6 | onNewGif: Msg.GIF_RECEIVED, 7 | }::toDispatchEventDictionary(); 8 | 9 | export default connect({ 10 | mapEventsToProps, 11 | })(PairOfGifPair); 12 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/app/messages.js: -------------------------------------------------------------------------------- 1 | import { toMessages } from 'engine/utils'; 2 | 3 | export default [ 4 | 'GIF_RECEIVED', 5 | 'INCREMENT_BY_TWO_TOGGLED', 6 | 'COUNTER_INCREMENTED', 7 | ]::toMessages(); 8 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/app/reducer.js: -------------------------------------------------------------------------------- 1 | import Msg from './messages'; 2 | 3 | const DEFAULT_STATE = { 4 | incrementByTwoEnabled: false, 5 | counterValue: 0, 6 | }; 7 | 8 | export default (state = DEFAULT_STATE, message) => { 9 | switch (message.type) { 10 | case Msg.INCREMENT_BY_TWO_TOGGLED: 11 | return { 12 | ...state, 13 | incrementByTwoEnabled: !state.incrementByTwoEnabled, 14 | }; 15 | case Msg.COUNTER_INCREMENTED: 16 | return { 17 | ...state, 18 | counterValue: state.counterValue + message.increment, 19 | }; 20 | default: 21 | return state; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/app/transformer.js: -------------------------------------------------------------------------------- 1 | import { toTransformer } from 'engine/utils'; 2 | import Msg from './messages'; 3 | 4 | export default [ 5 | [ 6 | Msg.GIF_RECEIVED, 7 | Msg.COUNTER_INCREMENTED, 8 | { 9 | create(sourceMessage, { getState }) { 10 | const { 11 | incrementByTwoEnabled, 12 | counterValue, 13 | } = getState(); 14 | 15 | const souldIncrementByTwo = incrementByTwoEnabled && counterValue >= 10; 16 | return { 17 | increment: souldIncrementByTwo ? 2 : 1, 18 | }; 19 | }, 20 | }, 21 | ], 22 | ]::toTransformer(); 23 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/engine/events.js: -------------------------------------------------------------------------------- 1 | import toMessages from './utils/toMessages'; 2 | 3 | export default [ 4 | 'INIT', 5 | 'CHANGE', 6 | ]::toMessages(); 7 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/engine/index.js: -------------------------------------------------------------------------------- 1 | export EVENTS from './events'; 2 | export createEngine from './createEngine'; 3 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/engine/react/Provider.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { engineType } from './propTypes'; 3 | 4 | export default class Provider extends Component { 5 | getChildContext() { 6 | return { 7 | engine: this.props.engine, 8 | }; 9 | } 10 | 11 | render() { 12 | return (
{this.props.children}
); 13 | } 14 | } 15 | 16 | Provider.propTypes = { 17 | children: PropTypes.node, 18 | engine: engineType, 19 | }; 20 | 21 | Provider.childContextTypes = { 22 | engine: engineType, 23 | }; 24 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/engine/react/helpers.js: -------------------------------------------------------------------------------- 1 | export function ensureIsFunction(func, name) { 2 | if (!func || typeof func !== 'function') { 3 | throw new Error(`${name} must be a function`); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/engine/react/index.js: -------------------------------------------------------------------------------- 1 | export Provider from './Provider'; 2 | export connect from './connect'; 3 | export assemble from './assemble'; 4 | export toDispatchEventDictionary from './toDispatchEventDictionary'; 5 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/engine/react/propTypes.js: -------------------------------------------------------------------------------- 1 | import { PropTypes } from 'react'; 2 | 3 | export const engineType = PropTypes.object; 4 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/engine/react/toDispatchEventDictionary.js: -------------------------------------------------------------------------------- 1 | export default function toDispatchEventDictionary() { 2 | const keyToEventDictionary = this; 3 | return ({ 4 | dispatch, 5 | getEmitterProps, 6 | }) => { 7 | const resultDictionary = {}; 8 | Object.entries(keyToEventDictionary).forEach(([key, type]) => { 9 | resultDictionary[key] = (...args) => dispatch({ 10 | type, 11 | args, 12 | getEmitterProps, 13 | }); 14 | }); 15 | return resultDictionary; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/engine/utils/index.js: -------------------------------------------------------------------------------- 1 | export toConsumer from './toConsumer'; 2 | export toMessages from './toMessages'; 3 | export toMapper from './toMapper'; 4 | export toTransformer from './toTransformer'; 5 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/engine/utils/toConsumer.js: -------------------------------------------------------------------------------- 1 | export default function toConsumer() { 2 | return (message, consumeOptions) => { 3 | const { type } = message; 4 | this.forEach(consumerDeclaration => { 5 | const [expectedType, consume] = consumerDeclaration; 6 | if (expectedType === type) { 7 | consume(message, consumeOptions); 8 | } 9 | }); 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/engine/utils/toMapper.js: -------------------------------------------------------------------------------- 1 | export default function toMapper() { 2 | const declarations = this; 3 | for (const declaration of declarations) { 4 | const [expectedType] = declaration; 5 | if (!expectedType) { 6 | const error = new Error('Missing expected type in mapper declaration'); 7 | error.declaration = declaration; 8 | throw error; 9 | } 10 | } 11 | 12 | return function map(event, mapOptions) { 13 | const { type } = event; 14 | 15 | for (const tranformerDeclaration of declarations) { 16 | const [expectedType, declarationOptions = {}] = tranformerDeclaration; 17 | if (expectedType === type) { 18 | return { 19 | ...declarationOptions.map(event, mapOptions), 20 | type, 21 | }; 22 | } 23 | } 24 | 25 | return event; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/engine/utils/toMessages.js: -------------------------------------------------------------------------------- 1 | export default function toMessages() { 2 | return this.reduce( 3 | (object, key) => 4 | Object.assign(object, { 5 | [key]: { 6 | toString() { 7 | return key; 8 | }, 9 | toJSON() { 10 | return JSON.stringify(key); 11 | }, 12 | }, 13 | }), 14 | {} 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/Button/component.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const Button = ({ 4 | value, 5 | onClick, 6 | }) => ( 7 | 11 | ); 12 | 13 | Button.propTypes = { 14 | value: PropTypes.bool.isRequired, 15 | onClick: PropTypes.func, 16 | }; 17 | 18 | export default Button; 19 | 20 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/Button/index.js: -------------------------------------------------------------------------------- 1 | export default from './component'; 2 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/Counter/component.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const Counter = ({ 4 | value, 5 | }) => (
Value: {value}
); 6 | 7 | Counter.propTypes = { 8 | value: PropTypes.number.isRequired, 9 | }; 10 | 11 | export default Counter; 12 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/Counter/index.js: -------------------------------------------------------------------------------- 1 | export default from './component'; 2 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/GifList/ConnectedGifViewer/index.js: -------------------------------------------------------------------------------- 1 | import { connect, toDispatchEventDictionary } from 'engine/react'; 2 | import GifViewer from 'modules/GifViewer'; 3 | import Msg from '../messages'; 4 | 5 | const mapEventsToProps = { 6 | onNewGif: Msg.GIF_RECEIVED, 7 | }::toDispatchEventDictionary(); 8 | 9 | const ConnectedGifViewer = connect({ 10 | mapEventsToProps, 11 | })(GifViewer); 12 | 13 | ConnectedGifViewer.propTypes = { 14 | ...ConnectedGifViewer.propTypes, 15 | topic: GifViewer.propTypes.topic, 16 | }; 17 | 18 | export default ConnectedGifViewer; 19 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/GifList/component.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import GifViewer from './ConnectedGifViewer'; 3 | 4 | const GifList = ( 5 | { 6 | topics, 7 | newTopic, 8 | onAddTopic, 9 | onNewTopicChange, 10 | } 11 | ) => (
12 | 17 |
18 | 19 |
20 | {topics.map((topic, index) => ( 21 | 22 | 23 | 24 | ))} 25 |
); 26 | 27 | GifList.propTypes = { 28 | topics: PropTypes.arrayOf(GifViewer.propTypes.topic), 29 | newTopic: PropTypes.string, 30 | onAddTopic: PropTypes.func.isRequired, 31 | onNewTopicChange: PropTypes.func.isRequired, 32 | }; 33 | 34 | export default GifList; 35 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/GifList/consumer.js: -------------------------------------------------------------------------------- 1 | import { toConsumer } from 'engine/utils'; 2 | import Msg from './messages'; 3 | 4 | export default [ 5 | [ 6 | Msg.GIF_RECEIVED, 7 | (message, { getApiProps }) => { 8 | const { onNewGif } = getApiProps(); 9 | if (onNewGif) { 10 | onNewGif(message); 11 | } 12 | }, 13 | ], 14 | ]::toConsumer(); 15 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/GifList/messages.js: -------------------------------------------------------------------------------- 1 | import { toMessages } from 'engine/utils'; 2 | 3 | export default [ 4 | 'NEW_TOPIC_CHANGED', 5 | 'TOPIC_ADDED', 6 | 'GIF_RECEIVED', 7 | ]::toMessages(); 8 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/GifList/reducer.js: -------------------------------------------------------------------------------- 1 | import Msg from './messages'; 2 | 3 | const DEFAULT_STATE = { 4 | newTopic: '', 5 | topics: [], 6 | }; 7 | 8 | export default (state = DEFAULT_STATE, message) => { 9 | switch (message.type) { 10 | case Msg.NEW_TOPIC_CHANGED: { 11 | return { 12 | ...state, 13 | newTopic: message.newTopic, 14 | }; 15 | } 16 | case Msg.TOPIC_ADDED: { 17 | return { 18 | ...state, 19 | newTopic: '', 20 | topics: [...state.topics, message.topic], 21 | }; 22 | } 23 | default: 24 | return state; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/GifList/transformer.js: -------------------------------------------------------------------------------- 1 | import { EVENTS as ENGINE_EVENTS } from 'engine'; 2 | import { toTransformer } from 'engine/utils'; 3 | import Msg from './messages'; 4 | 5 | export default [ 6 | [ 7 | ENGINE_EVENTS.INIT, 8 | Msg.TOPIC_ADDED, 9 | { 10 | *create( 11 | source, 12 | { getApiProps } 13 | ) { 14 | const { 15 | defaultTopics = [], 16 | } = getApiProps(); 17 | for (const topic of defaultTopics) { 18 | yield { 19 | topic, 20 | }; 21 | } 22 | }, 23 | }, 24 | ], 25 | ]::toTransformer(); 26 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/GifPair/ConnectedGifViewer/index.js: -------------------------------------------------------------------------------- 1 | import { connect, toDispatchEventDictionary } from 'engine/react'; 2 | import GifViewer from 'modules/GifViewer'; 3 | import Msg from '../messages'; 4 | 5 | const mapEventsToProps = { 6 | onNewGif: Msg.GIF_RECEIVED, 7 | }::toDispatchEventDictionary(); 8 | 9 | const ConnectedGifViewer = connect({ 10 | mapEventsToProps, 11 | })(GifViewer); 12 | 13 | ConnectedGifViewer.propTypes = { 14 | ...ConnectedGifViewer.propTypes, 15 | topic: GifViewer.propTypes.topic, 16 | }; 17 | 18 | export default ConnectedGifViewer; 19 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/GifPair/component.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import GifViewer from './ConnectedGifViewer'; 3 | 4 | const GifPair = ({ 5 | topics, 6 | }) => (
7 | 8 | 9 | 10 | 11 | 12 | 13 |
); 14 | 15 | GifPair.propTypes = { 16 | topics: PropTypes.arrayOf(GifViewer.propTypes.topic), 17 | }; 18 | 19 | export default GifPair; 20 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/GifPair/consumer.js: -------------------------------------------------------------------------------- 1 | import { toConsumer } from 'engine/utils'; 2 | import Msg from './messages'; 3 | 4 | export default [ 5 | [ 6 | Msg.GIF_RECEIVED, 7 | (message, { getApiProps }) => { 8 | const { onNewGif } = getApiProps(); 9 | if (onNewGif) { 10 | onNewGif(message); 11 | } 12 | }, 13 | ], 14 | ]::toConsumer(); 15 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/GifPair/index.js: -------------------------------------------------------------------------------- 1 | import { PropTypes } from 'react'; 2 | import { assemble } from 'engine/react'; 3 | import { createEngine } from 'engine'; 4 | import GifPair from './component'; 5 | import consumer from './consumer'; 6 | import mapper from './mapper'; 7 | 8 | const AssembledGifPair = assemble({ 9 | engineFactory(engineOptions) { 10 | return createEngine({ 11 | ...engineOptions, 12 | mapper, 13 | consumer, 14 | }); 15 | }, 16 | })(GifPair); 17 | 18 | AssembledGifPair.propTypes = { 19 | topics: GifPair.propTypes.topics, 20 | onNewGif: PropTypes.func, 21 | }; 22 | 23 | export default AssembledGifPair; 24 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/GifPair/mapper.js: -------------------------------------------------------------------------------- 1 | import { toMapper } from 'engine/utils'; 2 | import Msg from './messages'; 3 | 4 | export default [ 5 | [ 6 | Msg.GIF_RECEIVED, 7 | { 8 | map({ 9 | args, 10 | getEmitterProps, 11 | }) { 12 | const [url] = args; 13 | return { 14 | url, 15 | index: getEmitterProps().index, 16 | }; 17 | }, 18 | }, 19 | ], 20 | ]::toMapper(); 21 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/GifPair/messages.js: -------------------------------------------------------------------------------- 1 | import { toMessages } from 'engine/utils'; 2 | 3 | export default [ 4 | 'GIF_RECEIVED', 5 | ]::toMessages(); 6 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/GifViewer/component.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const GifViewer = ({ 4 | topic, 5 | url, 6 | onRequestMore, 7 | }) => ( 8 |
9 |

{topic}

10 | {`Gif 11 | 12 |
13 | ); 14 | 15 | GifViewer.propTypes = { 16 | topic: PropTypes.string.isRequired, 17 | url: PropTypes.string.isRequired, 18 | onRequestMore: PropTypes.func.isRequired, 19 | }; 20 | 21 | export default GifViewer; 22 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/GifViewer/consumer.js: -------------------------------------------------------------------------------- 1 | import { toConsumer } from 'engine/utils'; 2 | import Msg from './messages'; 3 | 4 | export default [ 5 | [ 6 | Msg.GIF_REQUESTED, 7 | (message, { dispatch, getApiProps, getDependencies }) => { 8 | const { topic } = getApiProps(); 9 | getDependencies().service.fetchGif(topic).then(url => { 10 | dispatch({ 11 | type: Msg.GIF_RECEIVED, 12 | url, 13 | }); 14 | }); 15 | }, 16 | ], 17 | [ 18 | Msg.GIF_RECEIVED, 19 | (message, { getApiProps }) => { 20 | const { url } = message; 21 | const { onNewGif } = getApiProps(); 22 | if (onNewGif) { 23 | onNewGif(url); 24 | } 25 | }, 26 | ], 27 | ]::toConsumer(); 28 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/GifViewer/messages.js: -------------------------------------------------------------------------------- 1 | import { toMessages } from 'engine/utils'; 2 | 3 | export default [ 4 | 'GIF_REQUESTED', 5 | 'GIF_RECEIVED', 6 | ]::toMessages(); 7 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/GifViewer/reducer.js: -------------------------------------------------------------------------------- 1 | import Msg from './messages'; 2 | 3 | const DEFAULT_STATE = { 4 | url: '/assets/loading.gif', 5 | }; 6 | 7 | export default (state = DEFAULT_STATE, message) => { 8 | switch (message.type) { 9 | case Msg.GIF_RECEIVED: { 10 | return { 11 | ...state, 12 | url: message.url, 13 | }; 14 | } 15 | default: 16 | return state; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/GifViewer/service.js: -------------------------------------------------------------------------------- 1 | import 'whatwg-fetch'; 2 | 3 | export const fetchGif = topic => 4 | fetch(`http://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=${topic}`) 5 | .then(response => { 6 | if (response.status > 400) { 7 | throw new Error('Error while fetching from the server'); 8 | } else { 9 | return response.json(); 10 | } 11 | }) 12 | .then(body => body.data.image_url); 13 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/GifViewer/transformer.js: -------------------------------------------------------------------------------- 1 | import { EVENTS as ENGINE_EVENTS } from 'engine'; 2 | import { toTransformer } from 'engine/utils'; 3 | import Msg from './messages'; 4 | 5 | export default [ 6 | [ 7 | ENGINE_EVENTS.INIT, 8 | Msg.GIF_REQUESTED, 9 | ], 10 | ]::toTransformer(); 11 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/PairOfGifPair/ConnectedGifPair/index.js: -------------------------------------------------------------------------------- 1 | import { connect, toDispatchEventDictionary } from 'engine/react'; 2 | import GifPair from 'modules/GifPair'; 3 | import Msg from '../messages'; 4 | 5 | const mapEventsToProps = { 6 | onNewGif: Msg.GIF_RECEIVED, 7 | }::toDispatchEventDictionary(); 8 | 9 | const ConnectedGifPair = connect({ 10 | mapEventsToProps, 11 | })(GifPair); 12 | 13 | ConnectedGifPair.propTypes = { 14 | ...ConnectedGifPair.propTypes, 15 | topics: GifPair.propTypes.topics, 16 | }; 17 | 18 | export default ConnectedGifPair; 19 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/PairOfGifPair/component.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import GifPair from './ConnectedGifPair'; 3 | 4 | const PairOfGifPair = ({ 5 | topics, 6 | }) => (
7 | 8 | 9 |
); 10 | 11 | PairOfGifPair.propTypes = { 12 | topics: PropTypes.arrayOf(GifPair.propTypes.topics), 13 | }; 14 | 15 | export default PairOfGifPair; 16 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/PairOfGifPair/consumer.js: -------------------------------------------------------------------------------- 1 | import { toConsumer } from 'engine/utils'; 2 | import Msg from './messages'; 3 | 4 | export default [ 5 | [ 6 | Msg.GIF_RECEIVED, 7 | (message, { getApiProps }) => { 8 | const { onNewGif } = getApiProps(); 9 | if (onNewGif) { 10 | onNewGif(message); 11 | } 12 | }, 13 | ], 14 | ]::toConsumer(); 15 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/PairOfGifPair/index.js: -------------------------------------------------------------------------------- 1 | import { PropTypes } from 'react'; 2 | import { assemble } from 'engine/react'; 3 | import { createEngine } from 'engine'; 4 | import PairOfGifPair from './component'; 5 | import consumer from './consumer'; 6 | import mapper from './mapper'; 7 | 8 | const AssembledPairOfGifPair = assemble({ 9 | engineFactory(engineOptions) { 10 | return createEngine({ 11 | ...engineOptions, 12 | mapper, 13 | consumer, 14 | }); 15 | }, 16 | })(PairOfGifPair); 17 | 18 | AssembledPairOfGifPair.propTypes = { 19 | topics: PairOfGifPair.propTypes.topics, 20 | onNewGif: PropTypes.func, 21 | }; 22 | 23 | export default AssembledPairOfGifPair; 24 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/PairOfGifPair/mapper.js: -------------------------------------------------------------------------------- 1 | import { toMapper } from 'engine/utils'; 2 | import Msg from './messages'; 3 | 4 | export default [ 5 | [ 6 | Msg.GIF_RECEIVED, 7 | { 8 | map({ 9 | args, 10 | getEmitterProps, 11 | }) { 12 | return { 13 | ...args[0], 14 | pairIndex: getEmitterProps().index, 15 | }; 16 | }, 17 | }, 18 | ], 19 | ]::toMapper(); 20 | -------------------------------------------------------------------------------- /custom-architecture-volune/src/modules/PairOfGifPair/messages.js: -------------------------------------------------------------------------------- 1 | import { toMessages } from 'engine/utils'; 2 | 3 | export default [ 4 | 'GIF_RECEIVED', 5 | ]::toMessages(); 6 | -------------------------------------------------------------------------------- /deku-redux-iazel/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Redux Immutable Deku 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /deku-redux-iazel/demo/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slorber/scalable-frontend-with-elm-or-redux/f42646d2fc56949420eb802a85d851b0383f0069/deku-redux-iazel/demo/loading.gif -------------------------------------------------------------------------------- /deku-redux-iazel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iazel-deku-redux", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "challenge", 11 | "deku", 12 | "redux", 13 | "ecma6" 14 | ], 15 | "author": "Iazel", 16 | "license": "MIT", 17 | "dependencies": { 18 | "@f/create-element": "^1.0.0", 19 | "@f/empty-element": "^1.0.0", 20 | "deku": "^2.0.0-rc16", 21 | "immutable": "^3.8.1", 22 | "qwest": "^4.4.2", 23 | "redux": "^3.5.2" 24 | }, 25 | "devDependencies": { 26 | "babel-core": "^6.8.0", 27 | "babel-loader": "^6.2.4", 28 | "babel-preset-es2015": "^6.6.0", 29 | "webpack": "^1.13.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/button/actions.js: -------------------------------------------------------------------------------- 1 | import { taskAction, action } from '../utils/actions' 2 | 3 | const base = 'BUTTON_' 4 | 5 | export const TOGGLE = base + 'TOGGLE' 6 | export function toggle() { 7 | return taskAction(TOGGLE) 8 | } 9 | 10 | export const SET = base + 'SET' 11 | export function set(state) { 12 | return action(SET, state) 13 | } 14 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/button/component.js: -------------------------------------------------------------------------------- 1 | import { h } from 'deku' 2 | import { clickEvent } from '../utils/events' 3 | import { toggle } from './actions' 4 | 5 | const onClick = clickEvent(toggle) 6 | 7 | const comp = ({ props, dispatch }) => { 8 | const cs = props.active ? 'active' : 'inactive' 9 | const txt = props.active ? 'Active' : 'Inactive' 10 | 11 | return h('button', { class: 'the-button ' + cs, onClick: onClick(dispatch) }, [txt]) 12 | } 13 | 14 | export default (active) => ( 15 | h(comp, {active}) 16 | ) 17 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/button/model.js: -------------------------------------------------------------------------------- 1 | export const init = () => false 2 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/button/reducers.js: -------------------------------------------------------------------------------- 1 | import * as A from './actions' 2 | 3 | export function stateReducer(state, action) { 4 | if(action.type === A.SET) 5 | return action.payload 6 | 7 | return state 8 | } 9 | 10 | export function taskReducer(dispatch, action, state) { 11 | if(action.type === A.TOGGLE) 12 | return dispatch( A.set(!state) ) 13 | } 14 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/counter/actions.js: -------------------------------------------------------------------------------- 1 | import { action } from '../utils/actions' 2 | 3 | export const INC = 'COUNTER_INC' 4 | export function incBy(n) { 5 | return action(INC, n) 6 | } 7 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/counter/component.js: -------------------------------------------------------------------------------- 1 | import { h } from 'deku' 2 | 3 | export default (count) => ( 4 | h('span', {class: 'counter'}, [count]) 5 | ) 6 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/counter/model.js: -------------------------------------------------------------------------------- 1 | export const init = () => 0 2 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/counter/reducers.js: -------------------------------------------------------------------------------- 1 | import { INC } from './actions' 2 | 3 | export function stateReducer(state, action) { 4 | if(action.type === INC) 5 | state += action.payload 6 | 7 | return state 8 | } 9 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/deku-override/index.js: -------------------------------------------------------------------------------- 1 | import { action } from '../utils/actions' 2 | import cr from './render' 3 | 4 | export const createRender = cr 5 | 6 | export const wrapAction = (type, key, child) => action(type, {key, child}, Object.assign({wrapper: true}, child.meta)) 7 | 8 | export const overDisp = (type, key) => (disp) => (act) => disp( wrapAction(type, key, act) ) 9 | 10 | export const override = (type, key, comp) => { 11 | comp.override = overDisp(type, key) 12 | return comp 13 | } 14 | 15 | export function deepUnwrap(action) { 16 | while(action.meta && action.meta.wrapper === true) 17 | action = action.payload.child 18 | return action 19 | } 20 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import { createRender } from './deku-override' 3 | import task from './middlewares/task' 4 | import observer from './middlewares/observer' 5 | import Main from './main/component' 6 | import { initDef } from './main/model' 7 | import * as mr from './main/reducers' 8 | 9 | const store = createStore(mr.stateReducer, initDef(), applyMiddleware( 10 | observer( mr.observer ), 11 | task( mr.taskReducer ) 12 | )) 13 | const render = createRender(document.getElementById('mount'), store.dispatch) 14 | const app = () => render( Main(store.getState()) ) 15 | 16 | app() 17 | store.subscribe(app) 18 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/list/actions.js: -------------------------------------------------------------------------------- 1 | import { action, taskAction } from '../utils/actions' 2 | 3 | const base = 'LIST_' 4 | 5 | export const ITEM_ACTION = base + 'ITEM_ACTION' 6 | 7 | export const PUSH = base + 'PUSH' 8 | export function push(item) { 9 | return action(PUSH, item) 10 | } 11 | 12 | export const PUSH_NEW = base + 'PUSH_NEW' 13 | export function pushNew() { 14 | return taskAction(PUSH_NEW) 15 | } 16 | 17 | export const CHANGE_NEW_VAL = base + 'CHANGE_VAL' 18 | export function changeNewVal(field, value) { 19 | return action(CHANGE_NEW_VAL, {field, value}) 20 | } 21 | 22 | export const REMOVE = base + 'REMOVE' 23 | export function remove(idx) { 24 | return action(REMOVE, idx) 25 | } 26 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/list/make.js: -------------------------------------------------------------------------------- 1 | import List from './component' 2 | import * as m from './model' 3 | 4 | export default (comp, inputs, text) => (state) => ( 5 | List( m.getItems(state).map(comp).toArray(), inputs( m.getNew(state) ), text) 6 | ) 7 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/list/model.js: -------------------------------------------------------------------------------- 1 | import { List, Map } from 'immutable' 2 | 3 | const init = () => Map({ 4 | items: List(), 5 | inpts: Map() 6 | }) 7 | 8 | const get = (state, i) => state.getIn(['items', i]) 9 | const set = (state, i, item) => state.setIn(['items', i], item) 10 | const push = (state, item) => state.set('items', state.get('items').push(item)) 11 | const getItems = (state) => state.get('items') 12 | 13 | const remove = (state, i) => state.set('items', state.get('items').delete(i)) 14 | 15 | const getNew = (state) => state.get('inpts') 16 | const setNewVal = (state, field, value) => state.setIn(['inpts', field], value) 17 | 18 | export { 19 | get, 20 | set, 21 | init, 22 | push, 23 | remove, 24 | getNew, 25 | setNewVal, 26 | getItems 27 | } 28 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/main/actions.js: -------------------------------------------------------------------------------- 1 | import { taskAction } from '../utils/actions' 2 | 3 | export const TRIGGER_COUNTER = 'MAIN_TRIGGER_COUNTER' 4 | export function triggerCounter() { 5 | return taskAction(TRIGGER_COUNTER) 6 | } 7 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/middlewares/logger.js: -------------------------------------------------------------------------------- 1 | export default () => (next) => (action) => { console.log(action) } 2 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/middlewares/observer.js: -------------------------------------------------------------------------------- 1 | import { deepUnwrap } from '../deku-override' 2 | import { NEW_GIF } from '../rndgif/actions' 3 | import { triggerCounter } from '../main/actions' 4 | 5 | export default (observer) => ({ dispatch }) => (next) => (action) => { 6 | observer(dispatch, action) 7 | return next(action) 8 | } 9 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/middlewares/task.js: -------------------------------------------------------------------------------- 1 | export default (reducer) => ({ dispatch, getState }) => (next) => (action) => { 2 | if(action.meta && action.meta.task === true) 3 | return reducer(dispatch, action, getState()) 4 | 5 | return next(action) 6 | } 7 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/pair/actions.js: -------------------------------------------------------------------------------- 1 | export const ITEM_ACTION = 'PAIR_ITEM_ACTION' 2 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/pair/component.js: -------------------------------------------------------------------------------- 1 | import { h } from 'deku' 2 | import { override } from '../deku-override' 3 | import { ITEM_ACTION } from './actions' 4 | import { key } from './model' 5 | 6 | const comp = ({ props }) => ( 7 | h('div', {class: 'pair'}, [ 8 | h('div', {class: 'pair-item'}, [props.one]), 9 | h('div', {class: 'pair-item'}, [props.two]) 10 | ]) 11 | ) 12 | 13 | export default function Pair(one, two) { 14 | override(ITEM_ACTION, key.ONE, one) 15 | override(ITEM_ACTION, key.TWO, two) 16 | 17 | return h(comp, {one, two}) 18 | } 19 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/pair/make.js: -------------------------------------------------------------------------------- 1 | import Pair from './component' 2 | import { fst, snd } from './model' 3 | 4 | export default (Comp) => (state) => { 5 | const one = Comp(fst(state)) 6 | const two = Comp(snd(state)) 7 | return Pair(one, two) 8 | } 9 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/pair/model.js: -------------------------------------------------------------------------------- 1 | import { List } from 'immutable' 2 | import { get, set } from '../utils/meta-reducers' 3 | 4 | const key = { 5 | ONE: 0, 6 | TWO: 1 7 | } 8 | 9 | const init = (one, two) => List([one, two]) 10 | 11 | const fst = (state) => get(state, key.ONE) 12 | const snd = (state) => get(state, key.TWO) 13 | 14 | export { 15 | fst, 16 | snd, 17 | get, 18 | set, 19 | key, 20 | init 21 | } 22 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/pair/reducers.js: -------------------------------------------------------------------------------- 1 | import { metaStateReducer, metaTaskReducer } from '../utils/meta-reducers' 2 | import { ITEM_ACTION } from './actions' 3 | import { get, set } from './model' 4 | 5 | export const makeTaskReducer = metaTaskReducer(ITEM_ACTION, get) 6 | export const makeStateReducer = metaStateReducer(ITEM_ACTION, get, set) 7 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/rndgif-list/component.js: -------------------------------------------------------------------------------- 1 | import { h } from 'deku' 2 | import makeList from '../list/make' 3 | import RndGif from '../rndgif/component' 4 | 5 | const inputs = (fs) => [ 6 | h('div', {class: 'form-group'}, [ 7 | h('label', {class: 'form-label'}, ['Topic']), 8 | h('input', {name: 'topic', class:'form-control', value: fs.get('topic')}) 9 | ]) 10 | ] 11 | 12 | export default makeList(RndGif, inputs, 'Add Gif') 13 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/rndgif-list/model.js: -------------------------------------------------------------------------------- 1 | import { init } from '../list/model' 2 | 3 | export { 4 | init 5 | } 6 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/rndgif-list/reducers.js: -------------------------------------------------------------------------------- 1 | import { init } from '../rndgif/model' 2 | import { makeTaskReducer, makeStateReducer } from '../list/reducers' 3 | import * as r from '../rndgif/reducers' 4 | 5 | const initFields = (fields) => init(fields.get('topic')) 6 | 7 | export const taskReducer = makeTaskReducer(r.taskReducer, initFields) 8 | export const stateReducer = makeStateReducer(r.stateReducer) 9 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/rndgif-pair-pair/component.js: -------------------------------------------------------------------------------- 1 | import makePair from '../pair/make' 2 | import RndGifPair from '../rndgif-pair/component' 3 | 4 | export default makePair(RndGifPair) 5 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/rndgif-pair-pair/model.js: -------------------------------------------------------------------------------- 1 | import { init as pair } from '../pair/model' 2 | import { init as rgp } from '../rndgif-pair/model' 3 | 4 | export const init = (p1, p2, q1, q2) => pair( rgp(p1, p2), rgp(q1, q2) ) 5 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/rndgif-pair-pair/reducers.js: -------------------------------------------------------------------------------- 1 | import { makeStateReducer, makeTaskReducer } from '../pair/reducers' 2 | import * as rnd from '../rndgif-pair/reducers' 3 | 4 | export const taskReducer = makeTaskReducer( rnd.taskReducer ) 5 | export const stateReducer = makeStateReducer( rnd.stateReducer ) 6 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/rndgif-pair/component.js: -------------------------------------------------------------------------------- 1 | import makePair from '../pair/make' 2 | import RndGif from '../rndgif/component' 3 | 4 | export default makePair(RndGif) 5 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/rndgif-pair/model.js: -------------------------------------------------------------------------------- 1 | import { init as pair } from '../pair/model' 2 | import { init as rndgif } from '../rndgif/model' 3 | 4 | export const init = (topic1, topic2) => pair(rndgif(topic1), rndgif(topic2)) 5 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/rndgif-pair/reducers.js: -------------------------------------------------------------------------------- 1 | import { makeStateReducer, makeTaskReducer } from '../pair/reducers' 2 | import * as rnd from '../rndgif/reducers' 3 | 4 | export const taskReducer = makeTaskReducer( rnd.taskReducer ) 5 | export const stateReducer = makeStateReducer( rnd.stateReducer ) 6 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/rndgif/actions.js: -------------------------------------------------------------------------------- 1 | import { action, taskAction } from '../utils/actions' 2 | 3 | const base = 'RNDGIF_' 4 | 5 | export const LOADING = base + 'LOADING' 6 | export function loading(bool) { 7 | return action(LOADING, bool) 8 | } 9 | 10 | export const SETUP = base + 'SETUP' 11 | export function setup() { 12 | return taskAction(SETUP) 13 | } 14 | export const NEW_GIF = base + 'NEW_GIF' 15 | export function newGif() { 16 | return taskAction(NEW_GIF) 17 | } 18 | 19 | export const SET_GIF = base + 'SET_GIF' 20 | export function setGif(gif) { 21 | return action(SET_GIF, gif) 22 | } 23 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/rndgif/api.js: -------------------------------------------------------------------------------- 1 | import qwest from 'qwest' 2 | 3 | function fetchNewGif(topic) { 4 | const rq = qwest.get('https://api.giphy.com/v1/gifs/random', { 5 | api_key: 'dc6zaTOxFJmzC', 6 | topic 7 | }, {cache: true}) 8 | return rq.then((xhr, resp) => resp.data.image_url) 9 | } 10 | 11 | export default { 12 | fetchNewGif 13 | } 14 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/rndgif/component.js: -------------------------------------------------------------------------------- 1 | import { h } from 'deku' 2 | import { clickEvent } from '../utils/events' 3 | import { setup, newGif } from './actions' 4 | import * as m from './model' 5 | 6 | const more = clickEvent(newGif) 7 | 8 | const render = ({ props, dispatch }) => ( 9 | h('div', {class: 'random-gif'}, [ 10 | h('h2', {class: 'rg-topic'}, [props.topic]), 11 | h('div', {class: 'gif-wrapper'}, [ 12 | h('img', {class: 'rg-gif', src: props.gif}) 13 | ]), 14 | h('button', 15 | {class: 'rg-more', onClick: more(dispatch), disabled: props.loading}, 16 | ['More please!'] 17 | ) 18 | ]) 19 | ) 20 | 21 | const onCreate = ({ dispatch }) => { 22 | dispatch( setup() ) 23 | } 24 | 25 | const comp = { render, onCreate } 26 | 27 | export default (state) => h(comp, { 28 | gif: m.getGif(state), 29 | topic: m.getTopic(state), 30 | loading: m.isLoading(state) 31 | }) 32 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/rndgif/model.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable' 2 | 3 | export const init = (topic) => Map({ 4 | topic, 5 | loading: false, 6 | gif: 'loading.gif' 7 | }) 8 | 9 | export const getGif = (state) => state.get('gif') 10 | export const getTopic = (state) => state.get('topic') 11 | export const isLoading = (state) => state.get('loading') 12 | 13 | export const setGif = (state, val) => state.set('gif', val) 14 | export const setLoading = (state, val) => state.set('loading', val) 15 | export const setLoadingGif = (state) => state.set('gif', 'loading.gif') 16 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/unique/actions.js: -------------------------------------------------------------------------------- 1 | export const ITEM_ACTION = 'UNIQUE_ITEM_ACTION' 2 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/unique/component.js: -------------------------------------------------------------------------------- 1 | import { override } from '../deku-override' 2 | import { ITEM_ACTION } from './actions' 3 | 4 | export default (key, Comp) => (...state) => { 5 | const comp = Comp(...state) 6 | override(ITEM_ACTION, key, comp) 7 | return comp 8 | } 9 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/unique/model.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable' 2 | import { get, set } from '../utils/meta-reducers' 3 | 4 | const init = Map 5 | 6 | export { 7 | get, 8 | set, 9 | init 10 | } 11 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/unique/reducers.js: -------------------------------------------------------------------------------- 1 | import { metaStateReducer, metaTaskReducer } from '../utils/meta-reducers' 2 | import { ITEM_ACTION } from './actions' 3 | import { get, set } from './model' 4 | 5 | const getReducer = (reducers, key) => reducers[key] 6 | export const makeTaskReducer = metaTaskReducer(ITEM_ACTION, get, getReducer) 7 | export const makeStateReducer = metaStateReducer(ITEM_ACTION, get, set, getReducer) 8 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/utils/actions.js: -------------------------------------------------------------------------------- 1 | export const action = (type, payload = null, meta = null) => ( 2 | {type, payload, meta} 3 | ) 4 | 5 | export const taskAction = (type, payload) => action(type, payload, {task: true}) 6 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/utils/combine-reducers.js: -------------------------------------------------------------------------------- 1 | export const combineStateReducers = (rs) => (state, action) => state.withMutations(s => { 2 | for(let k in rs) { 3 | let val = rs[k](s.get(k), action) 4 | s.set(k, val) 5 | } 6 | }) 7 | 8 | export function combineTaskReducers(rs) { 9 | return (dispatch, action, state) => { 10 | let rv 11 | for(let k in rs) { 12 | let subs = k === '' ? state : state.get(k) 13 | // ~ debugger 14 | rv = rs[k](dispatch, action, subs) 15 | if(rv !== undefined) 16 | break 17 | } 18 | return rv 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/utils/events.js: -------------------------------------------------------------------------------- 1 | const pdEvent = (action) => (dispatch, ...rest) => (e) => { 2 | e.preventDefault() 3 | dispatch( action(...rest) ) 4 | } 5 | 6 | export const clickEvent = pdEvent 7 | export const submitEvent = pdEvent 8 | 9 | export const changeEvent = (action) => (dispatch, ...rest) => (e) => { 10 | dispatch(action(...rest, e.target.name, e.target.value)) 11 | } 12 | -------------------------------------------------------------------------------- /deku-redux-iazel/src/utils/meta-reducers.js: -------------------------------------------------------------------------------- 1 | import { overDisp } from '../deku-override' 2 | 3 | export const get = (state, key) => state.get(key) 4 | export const set = (state, key, val) => state.set(key, val) 5 | 6 | const id = (v) => v 7 | export const metaStateReducer = (ACTION, get, set, getReducer = id) => (reducers) => (state, action) => { 8 | if(action.type !== ACTION) 9 | return state 10 | 11 | const { key, child } = action.payload 12 | const reduce = getReducer(reducers, key) 13 | return set(state, key, reduce(get(state, key), child)) 14 | } 15 | 16 | export const metaTaskReducer = (ACTION, get, getReducer = id) => (reducers) => (dispatch, action, state) => { 17 | if(action.type !== ACTION) 18 | return 19 | 20 | const { key, child } = action.payload 21 | const reduce = getReducer(reducers, key) 22 | const disp = overDisp(ACTION, key)(dispatch) 23 | return reduce(disp, child, get(state, key)) 24 | } 25 | -------------------------------------------------------------------------------- /deku-redux-iazel/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports = { 4 |   entry: __dirname + '/src/index.js', 5 |   output: { 6 | path: __dirname + '/demo/', 7 | filename: 'bundle.js' 8 | }, 9 |   module: { 10 |     loaders: [ 11 | { 12 | test: /\.js$/, 13 | loader: 'babel-loader', 14 | exclude: /node_modules/, 15 | query: { 16 | presets: ['es2015'] 17 | } 18 | } 19 | ], 20 |   } 21 | }; 22 | -------------------------------------------------------------------------------- /elm-api-extend/Button.elm: -------------------------------------------------------------------------------- 1 | module Button where 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (style) 5 | import Html.Events exposing (onClick) 6 | 7 | type Action = Click 8 | 9 | type alias Model = Bool 10 | 11 | init : Model 12 | init = True 13 | 14 | update : Action -> Model -> Model 15 | update action model = not model 16 | 17 | view : Signal.Address Action -> Model -> Html 18 | view address model = 19 | button 20 | [ style [("backgroundColor", if model then "green" else "red")] 21 | , onClick address Click 22 | ] 23 | [ text "Click"] 24 | 25 | 26 | 27 | isActive : Model -> Bool 28 | isActive model = model -------------------------------------------------------------------------------- /elm-api-extend/Counter.elm: -------------------------------------------------------------------------------- 1 | module Counter where 2 | 3 | import Html exposing (..) 4 | 5 | type Action = Increment Bool 6 | 7 | type alias Model = Int 8 | 9 | init : Model 10 | init = 0 11 | 12 | update : Action -> Model -> Model 13 | update action model = 14 | case action of 15 | Increment True -> 16 | if model >= 10 then model + 2 else model + 1 17 | Increment False -> 18 | model + 1 19 | 20 | increment : Bool -> Model -> Model 21 | increment button model = 22 | update (Increment button) model 23 | 24 | 25 | view : Signal.Address Action -> Model -> Html 26 | view address model = 27 | div [] 28 | [ text <| toString model] 29 | -------------------------------------------------------------------------------- /elm-api-extend/README.md: -------------------------------------------------------------------------------- 1 | # Approach 2 | 3 | The main strategy for this approach is to extend the API of each component to include helpers. 4 | 5 | I have put the business logic into `Counter.elm` and exposed an `increment` function that increments the model based on the button state that it receives. 6 | 7 | The RandomGif family of components API is augmented with a function that answeres the question "is this action a NewGif?" 8 | 9 | RandomGif implements the direct `NewGif` detection, each subsequent enclosure implements its own version using it's child API. 10 | -------------------------------------------------------------------------------- /elm-api-extend/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "." 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "3.0.0 <= v < 4.0.0", 12 | "evancz/elm-effects": "2.0.1 <= v < 3.0.0", 13 | "evancz/elm-html": "4.0.2 <= v < 5.0.0", 14 | "evancz/elm-http": "3.0.0 <= v < 4.0.0", 15 | "evancz/start-app": "2.0.2 <= v < 3.0.0" 16 | }, 17 | "elm-version": "0.16.0 <= v < 0.17.0" 18 | } -------------------------------------------------------------------------------- /fractal-component/.babelrc.js: -------------------------------------------------------------------------------- 1 | const { NODE_ENV, BABEL_ENV } = process.env; 2 | 3 | const cjs = BABEL_ENV === "cjs" || NODE_ENV === "test"; 4 | 5 | module.exports = { 6 | presets: [ 7 | [ 8 | "@babel/preset-env", 9 | { 10 | loose: true, 11 | modules: false, 12 | forceAllTransforms: true 13 | } 14 | ], 15 | "@babel/preset-react" 16 | ], 17 | plugins: [ 18 | cjs && "@babel/plugin-transform-modules-commonjs", 19 | "@babel/plugin-proposal-object-rest-spread", 20 | "lodash" 21 | ].filter(Boolean) 22 | }; 23 | -------------------------------------------------------------------------------- /fractal-component/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Fractal Component Example App 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /fractal-component/src/components/App.style.js: -------------------------------------------------------------------------------- 1 | const styles = { 2 | table: { 3 | display: "flex", 4 | "flex-wrap": "wrap", 5 | margin: "0.2em 0.2em 0.2em 0.2em", 6 | padding: 0, 7 | "flex-direction": "rows" 8 | }, 9 | cell: { 10 | "box-sizing": "border-box", 11 | "flex-grow": 0, 12 | overflow: "hidden", 13 | padding: "0.2em 0.2em", 14 | "border-bottom": "none", 15 | display: "flex", 16 | "align-items": "center", 17 | "justify-content": "flex-start" 18 | } 19 | }; 20 | 21 | export default styles; -------------------------------------------------------------------------------- /fractal-component/src/components/Counter/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from "./types"; 2 | 3 | export function increaseCount() { 4 | return { 5 | type: actionTypes.INCREASE_COUNT 6 | }; 7 | } -------------------------------------------------------------------------------- /fractal-component/src/components/Counter/actions/types.js: -------------------------------------------------------------------------------- 1 | export const INCREASE_COUNT = Symbol("INCREASE_COUNT"); -------------------------------------------------------------------------------- /fractal-component/src/components/RandomGif/actions/types.js: -------------------------------------------------------------------------------- 1 | export const REQUEST_NEW_GIF = Symbol("REQUEST_NEW_GIF"); 2 | export const RECEIVE_NEW_GIF = Symbol("RECEIVE_NEW_GIF"); 3 | export const REQUEST_NEW_GIF_ERROR = Symbol("REQUEST_NEW_GIF_ERROR"); 4 | export const NEW_GIF = Symbol("NEW_GIF"); 5 | export const LOADING_START = Symbol("LOADING_START"); 6 | export const LOADING_COMPLETE = Symbol("LOADING_COMPLETE"); -------------------------------------------------------------------------------- /fractal-component/src/components/RandomGif/reducers/index.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from "../actions/types"; 2 | 3 | const reducer = function(state, action) { 4 | switch (action.type) { 5 | case actionTypes.REQUEST_NEW_GIF: 6 | return { 7 | ...state, 8 | isLoading: true, 9 | error: null 10 | }; 11 | case actionTypes.RECEIVE_NEW_GIF: 12 | const imageUrl = action.payload; 13 | return { 14 | ...state, 15 | isLoading: false, 16 | error: null, 17 | imageUrl 18 | }; 19 | case actionTypes.REQUEST_NEW_GIF_ERROR: 20 | return { 21 | ...state, 22 | isLoading: false, 23 | error: action.payload 24 | }; 25 | default: return state; 26 | } 27 | }; 28 | export default reducer; 29 | -------------------------------------------------------------------------------- /fractal-component/src/components/RandomGifPair/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from "./types"; 2 | 3 | export function requestNewPair() { 4 | return { 5 | type: actionTypes.REQUEST_NEW_PAIR 6 | }; 7 | } 8 | 9 | export function loadingStart() { 10 | return { 11 | type: actionTypes.LOADING_START 12 | }; 13 | } 14 | 15 | export function loadingComplete(error = null) { 16 | return { 17 | type: actionTypes.LOADING_COMPLETE, 18 | payload: { 19 | isSuccess: error ? false : true, 20 | error 21 | } 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /fractal-component/src/components/RandomGifPair/actions/types.js: -------------------------------------------------------------------------------- 1 | export const REQUEST_NEW_PAIR = Symbol("REQUEST_NEW_PAIR"); 2 | export const LOADING_START = Symbol("LOADING_START"); 3 | export const LOADING_COMPLETE = Symbol("LOADING_COMPLETE"); -------------------------------------------------------------------------------- /fractal-component/src/components/RandomGifPairPair/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from "./types"; 2 | 3 | export function requestNewPairPair() { 4 | return { 5 | type: actionTypes.REQUEST_NEW_PAIR_PAIR 6 | }; 7 | } 8 | 9 | export function loadingStart() { 10 | return { 11 | type: actionTypes.LOADING_START 12 | }; 13 | } 14 | 15 | export function loadingComplete(error = null) { 16 | return { 17 | type: actionTypes.LOADING_COMPLETE, 18 | payload: { 19 | isSuccess: error ? false : true, 20 | error 21 | } 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /fractal-component/src/components/RandomGifPairPair/actions/types.js: -------------------------------------------------------------------------------- 1 | export const REQUEST_NEW_PAIR_PAIR = Symbol("REQUEST_NEW_PAIR_PAIR"); 2 | export const LOADING_START = Symbol("LOADING_START"); 3 | export const LOADING_COMPLETE = Symbol("LOADING_COMPLETE"); -------------------------------------------------------------------------------- /fractal-component/src/components/ToggleButton/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from "./types"; 2 | 3 | export function click() { 4 | return { 5 | type: actionTypes.CLICK 6 | }; 7 | } -------------------------------------------------------------------------------- /fractal-component/src/components/ToggleButton/actions/types.js: -------------------------------------------------------------------------------- 1 | export const CLICK = Symbol("CLICK"); -------------------------------------------------------------------------------- /fractal-component/src/main.js: -------------------------------------------------------------------------------- 1 | import "@babel/polyfill"; 2 | 3 | import React from "react"; 4 | import ReactDOM from "react-dom"; 5 | 6 | import jss from "jss"; 7 | import jssDefaultPreset from "jss-preset-default"; 8 | 9 | import { AppContainerUtils } from "fractal-component"; 10 | import App from "./components/App"; 11 | 12 | jss.setup(jssDefaultPreset()); 13 | 14 | AppContainerUtils.createAppContainer(); 15 | 16 | ReactDOM.render(, document.getElementById("root")); 17 | -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/src/counter/actions.js: -------------------------------------------------------------------------------- 1 | export const INCREMENT = 'INCREMENT'; 2 | export const DECREMENT = 'DECREMENT'; 3 | 4 | export const increment = () => { 5 | return { 6 | type: INCREMENT 7 | } 8 | } 9 | 10 | export const decrement = () => { 11 | return { 12 | type: DECREMENT 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/src/counter/index.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions'; 2 | import reducer from './reducer'; 3 | import Container from './Container'; 4 | 5 | export { actions, reducer, Container }; 6 | -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/src/counter/reducer.js: -------------------------------------------------------------------------------- 1 | import { INCREMENT, DECREMENT } from './actions'; 2 | 3 | const initialState = 0; 4 | 5 | export default (state = initialState, action) => { 6 | switch (action.type) { 7 | case INCREMENT: 8 | return state+1; 9 | case DECREMENT: 10 | return state-1; 11 | default: 12 | return state; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/src/index.js: -------------------------------------------------------------------------------- 1 | import './styles.css' 2 | import 'babel-polyfill' 3 | import React from 'react' 4 | import { render } from 'react-dom' 5 | import { Provider } from 'react-redux' 6 | import { App, reducer } from './main' 7 | import { createStore, applyMiddleware } from 'redux' 8 | import createSagaMiddleware from 'redux-saga'; 9 | import createLogger from 'redux-logger'; 10 | import saga from './saga'; 11 | 12 | const logger = createLogger(); 13 | const sagaMiddleware = createSagaMiddleware(saga) 14 | 15 | const store = createStore(reducer, 16 | applyMiddleware(sagaMiddleware, logger) 17 | ); 18 | 19 | render( 20 | 21 | 22 | 23 | , 24 | document.getElementById('app') 25 | ) 26 | -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/src/randomGif/actions.js: -------------------------------------------------------------------------------- 1 | export const REQUEST_NEW_GIF = 'REQUEST_NEW_GIF'; 2 | export const NEW_GIF_SUCCESS = 'NEW_GIF_SUCCESS'; 3 | 4 | export const requestNewGif = (topic) => { 5 | return { 6 | type: REQUEST_NEW_GIF, 7 | topic 8 | }; 9 | }; 10 | 11 | export const newGifSuccess = (imageUrl) => { 12 | return { 13 | type: NEW_GIF_SUCCESS, 14 | imageUrl 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/src/randomGif/index.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions'; 2 | import reducer from './reducer'; 3 | import Container from './Container'; 4 | import saga from './saga'; 5 | 6 | export { actions, reducer, Container, saga }; 7 | -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/src/randomGif/reducer.js: -------------------------------------------------------------------------------- 1 | import { REQUEST_NEW_GIF, NEW_GIF_SUCCESS } from './actions' 2 | 3 | const initialState = { 4 | isPending: true, 5 | imageUrl: "" 6 | }; 7 | 8 | export default (state = initialState, action) => { 9 | switch (action.type) { 10 | case REQUEST_NEW_GIF: 11 | return { 12 | isPending: true, 13 | imageUrl: "" 14 | }; 15 | case NEW_GIF_SUCCESS: 16 | return { 17 | isPending: false, 18 | imageUrl: action.imageUrl 19 | }; 20 | default: 21 | return state; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/src/randomGif/saga.js: -------------------------------------------------------------------------------- 1 | import { call, fork, take, put } from 'redux-saga/effects' 2 | import { 3 | REQUEST_NEW_GIF, 4 | newGifSuccess 5 | } from './actions'; 6 | import fetchRandomGif from './gifEndpoint'; 7 | import { takeEvery } from 'redux-saga'; 8 | 9 | 10 | function* requestNewGif({topic}) { 11 | const imageUrl = yield call(fetchRandomGif, topic); 12 | yield put(newGifSuccess(imageUrl)); 13 | } 14 | 15 | function* watchRequestNewGif(){ 16 | yield* takeEvery(REQUEST_NEW_GIF, requestNewGif); 17 | } 18 | 19 | export default function* () { 20 | yield [ fork(watchRequestNewGif) ]; 21 | } 22 | -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/src/randomGifList/actions.js: -------------------------------------------------------------------------------- 1 | export const ADD_RANDOM_GIF = 'ADD_RANDOM_GIF'; 2 | 3 | export const addNewGif = (topic) => { 4 | return { 5 | type: ADD_RANDOM_GIF, 6 | topic 7 | }; 8 | }; 9 | 10 | export const CHANGE_TOPIC = 'CHANGE_TOPIC'; 11 | 12 | export const changeTopic = (topic) => { 13 | return { 14 | type: CHANGE_TOPIC, 15 | topic 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/src/randomGifList/index.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions'; 2 | import reducer from './reducer'; 3 | import Container from './Container'; 4 | 5 | export { actions, reducer, Container }; 6 | -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/src/randomGifList/reducer.js: -------------------------------------------------------------------------------- 1 | import { ADD_RANDOM_GIF, CHANGE_TOPIC } from './actions' 2 | 3 | const initialState = { 4 | count: 0, 5 | gifTopic:[], 6 | currentTopic: "" 7 | }; 8 | 9 | export default (state = initialState, action) => { 10 | switch (action.type) { 11 | case ADD_RANDOM_GIF: 12 | const count = state.count++; 13 | return { 14 | currentTopic: state.currentTopic, 15 | count, 16 | gifTopic: [...state.gifTopic, action.topic] 17 | }; 18 | case CHANGE_TOPIC: 19 | return { 20 | ...state, 21 | currentTopic: action.topic 22 | } 23 | default: 24 | return state; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/src/randomGifPair/Container.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Container as RandomGif } from '../randomGif' 3 | 4 | const Component = () => { 5 | return ( 6 |
7 |
8 | 10 |
11 |
12 | 14 |
15 |
16 | ) 17 | 18 | }; 19 | 20 | export default Component; 21 | -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/src/randomGifPair/index.js: -------------------------------------------------------------------------------- 1 | import Container from './Container'; 2 | 3 | export { Container }; 4 | -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/src/randomGifPairOfPair/Container.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Container as RandomGifPair } from '../randomGifPair' 3 | 4 | const Component = () => { 5 | return ( 6 |
7 |
8 |
Top
9 | 11 |
12 |
13 |
Bottom
14 | 16 |
17 |
18 | ) 19 | } 20 | 21 | export default Component; 22 | 23 | -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/src/randomGifPairOfPair/index.js: -------------------------------------------------------------------------------- 1 | import Container from './Container'; 2 | 3 | export { Container }; 4 | -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/src/saga.js: -------------------------------------------------------------------------------- 1 | import { select, put, fork } from 'redux-saga/effects'; 2 | import { increment } from './counter/actions'; 3 | import { takeEvery } from 'redux-saga'; 4 | import { REQUEST_NEW_GIF } from './randomGif/actions'; 5 | 6 | const getCounter = state => state.counter; 7 | const getButtonIsActive = state => state.button.isActive; 8 | 9 | function* onRequestNewGif() { 10 | const counter = yield select(getCounter); 11 | const isActive = yield select(getButtonIsActive); 12 | 13 | yield put(increment()); 14 | if( counter >= 10 && isActive) { 15 | yield put(increment()); 16 | } 17 | } 18 | 19 | function* watchRequestNewGif(){ 20 | yield* takeEvery(REQUEST_NEW_GIF, onRequestNewGif); 21 | } 22 | 23 | export default function* () { 24 | yield [ fork(watchRequestNewGif) ]; 25 | } -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif 3 | } 4 | 5 | .container { 6 | margin-bottom: 20px 7 | } 8 | -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/src/theButton/Container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { bindActionCreators } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import * as actions from './actions'; 5 | 6 | export const Component = ({ isActive, toggleButton }) => { 7 | return ( 8 | 12 | ); 13 | } 14 | 15 | export default connect( 16 | state => ({ isActive: state.button.isActive}), 17 | dispatch => bindActionCreators(actions, dispatch) 18 | )(Component) 19 | -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/src/theButton/actions.js: -------------------------------------------------------------------------------- 1 | export const TOGGLE_BUTTON_STATE = 'TOGGLE_BUTTON_STATE'; 2 | 3 | export const toggleButton = () => { 4 | return { 5 | type: TOGGLE_BUTTON_STATE 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/src/theButton/index.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions'; 2 | import reducer from './reducer'; 3 | import Container from './Container'; 4 | 5 | export { actions, reducer, Container }; 6 | -------------------------------------------------------------------------------- /localprovider-redux-saga-ganaraj/src/theButton/reducer.js: -------------------------------------------------------------------------------- 1 | import { TOGGLE_BUTTON_STATE } from './actions'; 2 | 3 | export const initialState = { 4 | isActive: false 5 | }; 6 | 7 | export default (state = initialState, action) => { 8 | switch (action.type) { 9 | case TOGGLE_BUTTON_STATE: 10 | return { 11 | isActive: !state.isActive 12 | }; 13 | default: 14 | return state; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /modux-js-pcreations/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://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 15 | npm-debug.log 16 | -------------------------------------------------------------------------------- /modux-js-pcreations/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). 2 | 3 | This project is an example usage of [modux-js](https://github.com/PCreations/modux-js) micro framework to create modular, fractable redux apps. 4 | 5 | It was created in response to the [Scalable frontend, with Elm or Redux](https://github.com/slorber/scalable-frontend-with-elm-or-redux) challenge by [Sébastien Lorber](https://github.com/slorber). All kudos to him to get me scratch my head very hard on this ! 6 | 7 | See it online 8 | ------------- 9 | https://pcreations.github.io/modux-js-examples/ 10 | 11 | Installation 12 | ---------------- 13 | 14 | `git clone https://github.com/PCreations/modux-js-examples.git` 15 | 16 | `cd modux-js-examples` 17 | 18 | `npm install` 19 | 20 | `npm run start` 21 | -------------------------------------------------------------------------------- /modux-js-pcreations/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slorber/scalable-frontend-with-elm-or-redux/f42646d2fc56949420eb802a85d851b0383f0069/modux-js-pcreations/public/favicon.ico -------------------------------------------------------------------------------- /modux-js-pcreations/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-intro { 18 | font-size: large; 19 | } 20 | 21 | @keyframes App-logo-spin { 22 | from { transform: rotate(0deg); } 23 | to { transform: rotate(360deg); } 24 | } 25 | -------------------------------------------------------------------------------- /modux-js-pcreations/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import moduxFactory from 'modux-js'; 3 | 4 | import logo from './logo.svg'; 5 | import './App.css'; 6 | import DevTools from './DevTools'; 7 | import root from './root' 8 | 9 | class App extends Component { 10 | render() { 11 | return ( 12 |
13 |
14 | 15 |
16 |
17 | logo 18 |

Welcome to React

19 |
20 |

21 | 22 |

23 |
24 | ); 25 | } 26 | } 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /modux-js-pcreations/src/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createDevTools } from 'redux-devtools' 3 | import LogMonitor from 'redux-devtools-log-monitor' 4 | import DockMonitor from 'redux-devtools-dock-monitor' 5 | 6 | 7 | export default createDevTools( 8 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /modux-js-pcreations/src/gif-viewer/effects.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch' 2 | 3 | export const fetchGif = (topic) => 4 | fetch(`https://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=${topic}`) 5 | .then(res => res.json()) 6 | .then(body => typeof body.data === 'undefined' ? '' : body.data.image_url) 7 | -------------------------------------------------------------------------------- /modux-js-pcreations/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /modux-js-pcreations/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux' 3 | import ReactDOM from 'react-dom'; 4 | 5 | import App from './App'; 6 | import configureStore from './configureStore' 7 | import './index.css'; 8 | 9 | const store = configureStore() 10 | 11 | ReactDOM.render( 12 | 13 | 14 | , 15 | document.getElementById('root') 16 | ); 17 | -------------------------------------------------------------------------------- /react-opaque-components-bodhi/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sample App 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /react-opaque-components-bodhi/server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var WebpackDevServer = require('webpack-dev-server'); 3 | var config = require('./webpack.config'); 4 | 5 | new WebpackDevServer(webpack(config), { 6 | publicPath: config.output.publicPath, 7 | hot: true, 8 | historyApiFallback: true 9 | }).listen(3000, 'localhost', function (err, result) { 10 | if (err) { 11 | return console.log(err); 12 | } 13 | 14 | console.log('Listening at http://localhost:3000/'); 15 | }); 16 | -------------------------------------------------------------------------------- /react-opaque-components-bodhi/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { NewGif, GifPair, PairPair } from './Gif'; 3 | import { App as Button } from './Button'; 4 | import { App as Counter } from './Counter'; 5 | 6 | export default class App extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.onNewGif = this.onNewGif.bind(this); 10 | } 11 | onNewGif() { 12 | const state = this.refs.button.buttonState(); 13 | this.refs.counter.increment(state); 14 | } 15 | render() { 16 | return ( 17 |
18 | 19 | 20 | 21 |
24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /react-opaque-components-bodhi/src/Button.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class App extends Component { 4 | constructor(props) { 5 | super(props) 6 | this.state = { 7 | toggled: false 8 | } 9 | this._toggle = this._toggle.bind(this); 10 | } 11 | _toggle() { 12 | this.setState({ toggled: !this.state.toggled }) 13 | } 14 | render() { 15 | const toggled = this.state.toggled; 16 | return ; 19 | } 20 | buttonState() { 21 | return this.state.toggled; 22 | } 23 | } 24 | 25 | export { 26 | App 27 | } 28 | -------------------------------------------------------------------------------- /react-opaque-components-bodhi/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /react-opaque-components-bodhi/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | entry: [ 7 | 'webpack-dev-server/client?http://localhost:3000', 8 | 'webpack/hot/only-dev-server', 9 | './src/index' 10 | ], 11 | output: { 12 | path: path.join(__dirname, 'dist'), 13 | filename: 'bundle.js', 14 | publicPath: '/static/' 15 | }, 16 | plugins: [ 17 | new webpack.HotModuleReplacementPlugin(), 18 | new webpack.ProvidePlugin({ 19 | 'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch' 20 | }) 21 | ], 22 | module: { 23 | loaders: [{ 24 | test: /\.js$/, 25 | loaders: ['react-hot', 'babel'], 26 | include: path.join(__dirname, 'src') 27 | }] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /redux+petux-tempname11/.flowconfig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slorber/scalable-frontend-with-elm-or-redux/f42646d2fc56949420eb802a85d851b0383f0069/redux+petux-tempname11/.flowconfig -------------------------------------------------------------------------------- /redux+petux-tempname11/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://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 15 | npm-debug.log 16 | -------------------------------------------------------------------------------- /redux+petux-tempname11/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "petux-tempname11", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "flow-bin": "^0.37.4", 7 | "react-scripts": "0.8.4" 8 | }, 9 | "dependencies": { 10 | "petux": "1.0.0-beta.0", 11 | "react": "^15.4.1", 12 | "react-dom": "^15.4.1", 13 | "react-redux": "^5.0.1", 14 | "redux": "^3.6.0" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test --env=jsdom", 20 | "eject": "react-scripts eject" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /redux+petux-tempname11/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slorber/scalable-frontend-with-elm-or-redux/f42646d2fc56949420eb802a85d851b0383f0069/redux+petux-tempname11/public/favicon.ico -------------------------------------------------------------------------------- /redux+petux-tempname11/src/Button/Component.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react'; 3 | import type { Action, State } from './types'; 4 | 5 | export default class RandomGif extends React.PureComponent { 6 | props: { 7 | state: State, 8 | dispatch: (Action) => void, 9 | } 10 | click = () => { this.props.dispatch({ type: 'CLICKED' }); } 11 | render() { 12 | const { state } = this.props; 13 | return ( 14 | 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /redux+petux-tempname11/src/Button/reducer.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { Action, State } from './types'; 3 | 4 | export default { 5 | reducer(state: State, action: Action): State { 6 | switch (action.type) { 7 | case 'CLICKED': 8 | return !state; 9 | default: 10 | return state; 11 | } 12 | }, 13 | initial: true, 14 | } 15 | -------------------------------------------------------------------------------- /redux+petux-tempname11/src/Button/types.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export type Action = { type: 'CLICKED' }; 4 | export type State = boolean; 5 | -------------------------------------------------------------------------------- /redux+petux-tempname11/src/Counter/Component.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react'; 3 | import type { Action, State } from './types'; 4 | 5 | export default class Counter extends React.PureComponent { 6 | props: { 7 | state: State, 8 | dispatch: (Action) => void, 9 | } 10 | increment = () => { this.props.dispatch({ type: 'INCREMENT_CLICKED' }); } 11 | decrement = () => { this.props.dispatch({ type: 'DECREMENT_CLICKED' }); } 12 | render() { 13 | const { state } = this.props; 14 | return ( 15 |
16 |
22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /redux+petux-tempname11/src/Counter/reducer.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { Action, State } from './types'; 3 | 4 | export default { 5 | reducer(state: State, action: Action): State { 6 | switch (action.type) { 7 | case 'INCREMENT_CLICKED': 8 | return state + 1; 9 | case 'DECREMENT_CLICKED': 10 | return state - 1; 11 | default: 12 | return state; 13 | } 14 | }, 15 | initial: 0, 16 | }; 17 | -------------------------------------------------------------------------------- /redux+petux-tempname11/src/Counter/types.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export type Action = 4 | | { type: 'INCREMENT_CLICKED' } 5 | | { type: 'DECREMENT_CLICKED' } 6 | ; 7 | 8 | export type State = number; 9 | -------------------------------------------------------------------------------- /redux+petux-tempname11/src/Pair/types.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export type ActionOf = 4 | | { type: 'FIRST' , payload: A } 5 | | { type: 'SECOND', payload: A } 6 | ; 7 | 8 | export type StateOf = [S, S]; 9 | -------------------------------------------------------------------------------- /redux+petux-tempname11/src/RandomGif/types.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import RoutedEffect from '../RoutedEffect'; 3 | 4 | export type Action = 5 | | { type: 'MORE_BUTTON_CLICKED' } 6 | | { type: 'REQUEST_SUCCEDED', src: string } 7 | | { type: 'REQUEST_FAILED' } 8 | ; 9 | 10 | export type State = { 11 | topic: string, 12 | status: 'loading' | 'failed' | { src: string }, 13 | }; 14 | 15 | type BaseEffect = { kind: 'REQUEST_NEW_GIF', topic: string }; 16 | 17 | export type Effect = RoutedEffect; 18 | -------------------------------------------------------------------------------- /redux+petux-tempname11/src/RoutedEffect.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { Emit } from 'petux'; 3 | 4 | export default class RoutedEffect { 5 | base: E 6 | route: A => B 7 | constructor(base: E, route: A => B) { 8 | this.base = base; 9 | this.route = route; 10 | } 11 | static create(base: E1): RoutedEffect { 12 | return new RoutedEffect(base, x => x); 13 | } 14 | map(f: B => C): RoutedEffect { 15 | const { base, route } = this; 16 | return new RoutedEffect(base, a => f(route(a))); 17 | } 18 | } 19 | 20 | export function emap(f: C => B): 21 | Emit> => Emit> 22 | { 23 | return emit => e => emit(e.map(f)); 24 | } 25 | -------------------------------------------------------------------------------- /redux+petux-tempname11/src/local-effects.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { Emit } from 'petux'; 3 | 4 | type Computation = () => R; 5 | 6 | export default function( 7 | localHandler: (E, S) => S 8 | ): {| 9 | emit: Emit, 10 | withLocal: (S, Computation) => [R, S], 11 | |} { 12 | let local; 13 | 14 | function emit(effect) { 15 | local = localHandler(effect, local); 16 | } 17 | 18 | function withLocal(initial, computation) { 19 | local = initial; 20 | const result = computation(); 21 | return [result, local]; 22 | } 23 | 24 | return { emit, withLocal }; 25 | } 26 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-0"], 3 | "plugins": [ 4 | "transform-runtime", 5 | "transform-decorators-legacy", 6 | "lodash" 7 | ], 8 | "env": { 9 | "development": { 10 | "presets": ["react-hmre"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | server.js 3 | webpack.*.js 4 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "jsx": true, 4 | "modules": true 5 | }, 6 | "env": { 7 | "browser": true, 8 | "node": true 9 | }, 10 | "parser": "babel-eslint", 11 | "rules": { 12 | "quotes": [2, "single"], 13 | "strict": [2, "never"], 14 | "babel/generator-star-spacing": 1, 15 | "babel/new-cap": 1, 16 | "babel/object-shorthand": 1, 17 | "babel/arrow-parens": 1, 18 | "babel/no-await-in-loop": 1, 19 | "react/jsx-uses-react": 2, 20 | "react/jsx-uses-vars": 2, 21 | "react/react-in-jsx-scope": 2 22 | }, 23 | "plugins": [ 24 | "babel", 25 | "react" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | npm-debug.log 4 | 5 | bower_components 6 | node_modules 7 | 8 | config.js 9 | dist 10 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/README.md: -------------------------------------------------------------------------------- 1 | # redux-architecture-challenge 2 | 3 | This repo is my solution for [the scalable frontend architecture challenge](https://github.com/slorber/scalable-frontend-with-elm-or-redux). 4 | 5 | [Inventing on Principle](https://www.youtube.com/watch?v=PUv66718DII) 6 | 7 | My principle: 8 | 1. immutable data 9 | 2. pure function 10 | 3. high order function 11 | 12 | IMO, Apply these basis functional programming technique, we don't need any extra concept. 13 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var webpack = require('webpack'); 3 | var webpackConfig = require('./webpack.development'); 4 | 5 | var app = express(); 6 | var compiler = webpack(webpackConfig); 7 | 8 | app.use(require('webpack-dev-middleware')(compiler, { 9 | stats: { 10 | colors: true, 11 | }, 12 | })); 13 | 14 | app.use(require('webpack-hot-middleware')(compiler)); 15 | 16 | app.listen(process.env.PORT || 3000, function(err) { 17 | if (err) { 18 | console.log(err); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/helpers/createStore.js: -------------------------------------------------------------------------------- 1 | import {createStore, applyMiddleware, compose} from 'redux'; 2 | import {install} from '@jarvisaoieong/redux-loop'; 3 | import createLogger from '@jarvisaoieong/redux-logger'; 4 | 5 | export default (reducer, initialState) => 6 | createStore(reducer, initialState, compose( 7 | install(), 8 | applyMiddleware(createLogger({collapsed: true})), 9 | )) 10 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/helpers/superagent.js: -------------------------------------------------------------------------------- 1 | import request from 'superagent'; 2 | import Promise from 'bluebird'; 3 | 4 | Promise.promisifyAll(request); 5 | 6 | export default request; 7 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | redux-architecture 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'react-dom'; 3 | import {Provider} from 'react-redux'; 4 | import createStore from './helpers/createStore'; 5 | import {Main, reducer, init} from 'modules/main'; 6 | 7 | const store = createStore(reducer, init()); 8 | 9 | render( 10 | 11 |
12 | 13 | , 14 | document.getElementById('app') 15 | ); 16 | 17 | if (process.env.NODE_ENV === 'development' && module.hot) { 18 | module.hot.accept('modules/main', () => { 19 | const {reducer: nextReducer} = require('modules/main'); 20 | store.replaceReducer(nextReducer); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/button/actions.js: -------------------------------------------------------------------------------- 1 | export const TOGGLE = 'TOGGLE'; 2 | 3 | export const toggle = () => ({ 4 | type: TOGGLE, 5 | }); 6 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/button/components/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {toggle} from '../actions'; 3 | 4 | export default ({model, dispatch}) => 5 | 13 | 14 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/button/index.js: -------------------------------------------------------------------------------- 1 | export Button from './components/Button'; 2 | export init from './init'; 3 | export reducer, {initialState} from './reducer'; 4 | export { 5 | TOGGLE, 6 | toggle, 7 | } from './actions'; 8 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/button/init.js: -------------------------------------------------------------------------------- 1 | export default (bool) => !!bool 2 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/button/reducer.js: -------------------------------------------------------------------------------- 1 | import {TOGGLE} from './actions'; 2 | 3 | export const initialState = false 4 | 5 | export default (state = initialState, action) => { 6 | if (action.type === TOGGLE) { 7 | return !state; 8 | } 9 | return state; 10 | } 11 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/counter/actions.js: -------------------------------------------------------------------------------- 1 | export const INC = 'INC'; 2 | export const DEC = 'DEC'; 3 | 4 | export const inc = (value = 1) => ({ 5 | type: INC, 6 | value, 7 | }); 8 | 9 | export const dec = (value = 1) => ({ 10 | type: DEC, 11 | value, 12 | }); 13 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/counter/components/Counter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {inc, dec} from '../actions'; 3 | 4 | export default (props) => { 5 | const {model, dispatch} = props; 6 | return ( 7 |
8 | 14 | 17 | {model} 18 | 19 | 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/counter/index.js: -------------------------------------------------------------------------------- 1 | export Counter from './components/Counter'; 2 | export init from './init'; 3 | export reducer, {initialState} from './reducer'; 4 | export { 5 | INC, 6 | DEC, 7 | inc, 8 | dec, 9 | } from './actions'; 10 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/counter/init.js: -------------------------------------------------------------------------------- 1 | export default (count = 0) => count 2 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/counter/reducer.js: -------------------------------------------------------------------------------- 1 | import {INC, DEC} from './actions'; 2 | 3 | export const initialState = 0; 4 | 5 | export default (state = initialState, action) => { 6 | if (action.type === INC) { 7 | return state + action.value; 8 | } 9 | 10 | if (action.type === DEC) { 11 | return state - action.value; 12 | } 13 | 14 | return state; 15 | } 16 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/main/actions.js: -------------------------------------------------------------------------------- 1 | export const NEW_GIF_COUNT = 'NEW_GIF_COUNT'; 2 | 3 | export const newGifCount = () => ({ 4 | type: NEW_GIF_COUNT, 5 | }); 6 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/main/components/ButtonContainer.js: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux'; 2 | import {Button} from 'modules/button'; 3 | 4 | export default connect( 5 | (state) => ({ 6 | model: state.button, 7 | }) 8 | )(Button); 9 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/main/components/CounterContainer.js: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux'; 2 | import {Counter} from 'modules/counter'; 3 | 4 | export default connect( 5 | (state) => ({ 6 | model: state.counter, 7 | }) 8 | )(Counter); 9 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/main/components/RandomGifContainer.js: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux'; 2 | import {RandomGif} from 'modules/randomGif'; 3 | 4 | export default connect( 5 | (state) => ({ 6 | model: state.randomGif, 7 | }) 8 | )(RandomGif); 9 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/main/components/RandomGifListContainer.js: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux'; 2 | import {RandomGifList} from 'modules/randomGifList'; 3 | 4 | export default connect( 5 | (state) => ({ 6 | model: state.randomGifList, 7 | }) 8 | )(RandomGifList); 9 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/main/components/RandomGifPairContainer.js: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux'; 2 | import {RandomGifPair} from 'modules/randomGifPair'; 3 | 4 | export default connect( 5 | (state) => ({ 6 | model: state.randomGifPair, 7 | }) 8 | )(RandomGifPair); 9 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/main/components/RandomGifPairOfPairContainer.js: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux'; 2 | import {RandomGifPairOfPair} from 'modules/randomGifPairOfPair'; 3 | 4 | export default connect( 5 | (state) => ({ 6 | model: state.randomGifPairOfPair, 7 | }) 8 | )(RandomGifPairOfPair); 9 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/main/index.js: -------------------------------------------------------------------------------- 1 | export Main from './components/Main'; 2 | export init from './init'; 3 | export reducer from './reducer'; 4 | export { 5 | NEW_GIF_COUNT, 6 | newGifCount, 7 | } from './actions'; 8 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/main/newGifCountHor.js: -------------------------------------------------------------------------------- 1 | import {loop, Effects} from '@jarvisaoieong/redux-loop'; 2 | import _ from 'lodash'; 3 | import {NEW_GIF} from 'modules/randomGif'; 4 | import {newGifCount} from './actions'; 5 | 6 | export default (path, reducer) => (state, action) => { 7 | const {model, effect} = reducer(state, action); 8 | 9 | if (state !== model || !_.isEqual(effect, Effects.none())) { 10 | if (_.get(action, path) === NEW_GIF) { 11 | return loop( 12 | model 13 | , 14 | Effects.batch([ 15 | effect, 16 | Effects.constant(newGifCount()), 17 | ]) 18 | ); 19 | } 20 | } 21 | 22 | return loop(model, effect); 23 | } 24 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/randomGif/actions.js: -------------------------------------------------------------------------------- 1 | export const REQUEST_MORE = 'REQUEST_MORE'; 2 | export const REQUEST_ERROR = 'REQUEST_ERROR'; 3 | export const NEW_GIF = 'NEW_GIF'; 4 | 5 | export const requestMore = () => ({ 6 | type: REQUEST_MORE, 7 | }); 8 | 9 | export const requestError = (error) => ({ 10 | type: REQUEST_ERROR, 11 | error, 12 | }); 13 | 14 | export const newGif = (url) => ({ 15 | type: NEW_GIF, 16 | url, 17 | }); 18 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/randomGif/components/RandomGif.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import _ from 'lodash'; 3 | 4 | import {requestMore} from '../actions'; 5 | 6 | export default class RandomGif extends Component { 7 | 8 | render() { 9 | const {model, dispatch} = this.props; 10 | return ( 11 |
12 | {model.topic} 13 | 19 | 27 |
28 | ); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/randomGif/components/waiting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slorber/scalable-frontend-with-elm-or-redux/f42646d2fc56949420eb802a85d851b0383f0069/redux-architecture-jarvisaoieong/src/modules/randomGif/components/waiting.gif -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/randomGif/index.js: -------------------------------------------------------------------------------- 1 | export RandomGif from './components/RandomGif'; 2 | export init from './init'; 3 | export reducer, {initialState} from './reducer'; 4 | export { 5 | REQUEST_MORE, 6 | REQUEST_ERROR, 7 | NEW_GIF, 8 | requestMore, 9 | requestError, 10 | newGif, 11 | } from './actions'; 12 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/randomGif/init.js: -------------------------------------------------------------------------------- 1 | import {loop, Effects} from '@jarvisaoieong/redux-loop'; 2 | import {fetchRandomGif} from './tasks'; 3 | import {newGif} from './actions'; 4 | 5 | export default (topic) => { 6 | return loop({ 7 | topic, 8 | gifUrl: require('./components/waiting.gif'), 9 | }, 10 | Effects.promise(fetchRandomGif, topic) 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/randomGif/reducer.js: -------------------------------------------------------------------------------- 1 | import {loop, Effects} from '@jarvisaoieong/redux-loop'; 2 | 3 | import {REQUEST_MORE, NEW_GIF, newGif} from './actions'; 4 | import {fetchRandomGif} from './tasks'; 5 | 6 | export const initialState = { 7 | topic: '', 8 | gifUrl: require('./components/waiting.gif'), 9 | }; 10 | 11 | export default (state = initialState, action) => { 12 | if (action.type === REQUEST_MORE) { 13 | return loop( 14 | state 15 | , 16 | Effects.promise(fetchRandomGif, state.topic) 17 | ); 18 | }; 19 | 20 | if (action.type === NEW_GIF) { 21 | return loop({ 22 | ...state, 23 | gifUrl: action.url, 24 | }, 25 | Effects.none() 26 | ); 27 | }; 28 | 29 | return loop(state, Effects.none()); 30 | } 31 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/randomGif/tasks.js: -------------------------------------------------------------------------------- 1 | import request from 'helpers/superagent'; 2 | import _ from 'lodash'; 3 | import {newGif, requestError} from './actions'; 4 | 5 | export const fetchRandomGif = (topic) => { 6 | return request.get('https://api.giphy.com/v1/gifs/random') 7 | .query({ 8 | api_key: 'dc6zaTOxFJmzC', 9 | tag: topic, 10 | }) 11 | .endAsync() 12 | .then((res) => { 13 | const url = _.get(res, 'body.data.image_url'); 14 | return newGif(url); 15 | }) 16 | .catch(requestError); 17 | } 18 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/randomGifList/actions.js: -------------------------------------------------------------------------------- 1 | export const CREATE = 'CREATE_RANDOMGIFLIST'; 2 | export const MODIFY = 'MODIFY_RANDOMGIFLIST'; 3 | 4 | export const create = (topic) => ({ 5 | type: CREATE, 6 | topic 7 | }); 8 | 9 | export const modify = (id, action) => ({ 10 | type: MODIFY, 11 | id, 12 | action, 13 | }); 14 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/randomGifList/index.js: -------------------------------------------------------------------------------- 1 | export RandomGifList from './components/RandomGifList'; 2 | export init from './init'; 3 | export reducer, {initialState} from './reducer'; 4 | export { 5 | CREATE, 6 | MODIFY, 7 | create, 8 | modify, 9 | } from './actions'; 10 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/randomGifList/init.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import {loop, Effects} from '@jarvisaoieong/redux-loop'; 3 | import {init as randomGifInit} from 'modules/randomGif'; 4 | import {modify} from './actions'; 5 | 6 | export default (topicList = []) => { 7 | const gifLoopList = _.map(topicList, randomGifInit); 8 | 9 | return loop({ 10 | gifList: _.map(gifLoopList, (gifLoop, index) => ({ 11 | id: index, 12 | data: gifLoop.model, 13 | })), 14 | nextId: topicList.length + 1, 15 | }, 16 | Effects.batch(_.map(gifLoopList, (gifLoop, index) => 17 | Effects.map(gifLoop.effect, modify, index) 18 | )) 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/randomGifPair/actions.js: -------------------------------------------------------------------------------- 1 | export const MODIFY_FIRST = 'MODIFY_PAIR_FIRST_RANDOM_GIF'; 2 | export const MODIFY_SECOND = 'MODIFY_PAIR_SECOND_RANDOM_GIF'; 3 | 4 | export const modifyFirst = (action) => ({ 5 | type: MODIFY_FIRST, 6 | action, 7 | }); 8 | 9 | export const modifySecond = (action) => ({ 10 | type: MODIFY_SECOND, 11 | action, 12 | }); 13 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/randomGifPair/components/RandomGifPair.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | import {RandomGif} from 'modules/randomGif' 4 | import {modifyFirst, modifySecond} from '../actions'; 5 | 6 | export default class RandomGifPair extends Component { 7 | 8 | render() { 9 | const {model, dispatch} = this.props; 10 | 11 | return ( 12 |
13 |
14 | dispatch(modifyFirst(action)), 17 | }} /> 18 |
19 |
20 | dispatch(modifySecond(action)), 23 | }} /> 24 |
25 |
26 |
27 | ); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/randomGifPair/index.js: -------------------------------------------------------------------------------- 1 | export RandomGifPair from './components/RandomGifPair'; 2 | export init from './init'; 3 | export reducer, {initialState} from './reducer'; 4 | export { 5 | MODIFY_FIRST, 6 | MODIFY_SECOND, 7 | modifyFirst, 8 | modifySecond, 9 | } from './actions'; 10 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/randomGifPair/init.js: -------------------------------------------------------------------------------- 1 | import {loop, Effects} from '@jarvisaoieong/redux-loop'; 2 | 3 | import {init as randomGifInit} from 'modules/randomGif'; 4 | import {modifyFirst, modifySecond} from './actions'; 5 | 6 | export default (firstTopic, secondTopic) => { 7 | const { 8 | model: firstModel, 9 | effect: firstEffect, 10 | } = randomGifInit(firstTopic); 11 | 12 | const { 13 | model: secondModel, 14 | effect: secondEffect, 15 | } = randomGifInit(secondTopic); 16 | 17 | return loop({ 18 | first: firstModel, 19 | second: secondModel, 20 | }, 21 | Effects.batch([ 22 | Effects.map(firstEffect, modifyFirst), 23 | Effects.map(firstEffect, modifySecond), 24 | ]) 25 | ); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/randomGifPairOfPair/actions.js: -------------------------------------------------------------------------------- 1 | export const MODIFY_FIRST = 'MODIFY_PAIR_OF_PAIR_FIRST_RANDOM_GIF'; 2 | export const MODIFY_SECOND = 'MODIFY_PAIR_OF_PAIR_SECOND_RANDOM_GIF'; 3 | 4 | export const modifyFirst = (action) => ({ 5 | type: MODIFY_FIRST, 6 | action, 7 | }); 8 | 9 | export const modifySecond = (action) => ({ 10 | type: MODIFY_SECOND, 11 | action, 12 | }); 13 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/randomGifPairOfPair/index.js: -------------------------------------------------------------------------------- 1 | export RandomGifPairOfPair from './components/RandomGifPairOfPair'; 2 | export init from './init'; 3 | export reducer, {initialState} from './reducer'; 4 | export { 5 | MODIFY_FIRST, 6 | MODIFY_SECOND, 7 | modifyFirst, 8 | modifySecond, 9 | } from './actions'; 10 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/src/modules/randomGifPairOfPair/init.js: -------------------------------------------------------------------------------- 1 | import {loop, Effects} from '@jarvisaoieong/redux-loop'; 2 | 3 | import {init as randomGifPairInit} from 'modules/randomGifPair'; 4 | import {modifyFirst, modifySecond} from './actions'; 5 | 6 | export default (firstTopic, secondTopic) => { 7 | const { 8 | model: firstModel, 9 | effect: firstEffect, 10 | } = randomGifPairInit(firstTopic, firstTopic); 11 | 12 | const { 13 | model: secondModel, 14 | effect: secondEffect, 15 | } = randomGifPairInit(secondTopic, secondTopic); 16 | 17 | return loop({ 18 | first: firstModel, 19 | second: secondModel, 20 | }, 21 | Effects.batch([ 22 | Effects.map(firstEffect, modifyFirst), 23 | Effects.map(firstEffect, modifySecond), 24 | ]) 25 | ); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/webpack.development.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var merge = require('@ersinfotech/merge'); 3 | 4 | var webpackConfig = require('./webpack.config'); 5 | 6 | process.env.NODE_ENV = 'development'; 7 | 8 | module.exports = merge(webpackConfig, { 9 | devtool: 'eval', 10 | debug: true, 11 | entry: ['webpack-hot-middleware/client'], 12 | module: { 13 | loaders: [{ 14 | test: /\.css$/, 15 | loaders: ['style', 'css'], 16 | exclude: /components/, 17 | }], 18 | }, 19 | plugins: [ 20 | new webpack.optimize.OccurenceOrderPlugin(), 21 | new webpack.HotModuleReplacementPlugin(), 22 | new webpack.NoErrorsPlugin(), 23 | new webpack.DefinePlugin({ 24 | 'process.env': { 25 | NODE_ENV: '"development"', 26 | }, 27 | }), 28 | ], 29 | }); 30 | -------------------------------------------------------------------------------- /redux-architecture-jarvisaoieong/webpack.production.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var merge = require('@ersinfotech/merge'); 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | 5 | var webpackConfig = require('./webpack.config'); 6 | 7 | process.env.NODE_ENV = 'production'; 8 | 9 | module.exports = merge(webpackConfig, { 10 | module: { 11 | loaders: [{ 12 | test: /\.css$/, 13 | loader: ExtractTextPlugin.extract('style-loader', 'css-loader'), 14 | exclude: /components/, 15 | }], 16 | }, 17 | plugins: [ 18 | new ExtractTextPlugin('[contenthash].css', { 19 | allChunks: true, 20 | }), 21 | new webpack.optimize.UglifyJsPlugin(), 22 | new webpack.DefinePlugin({ 23 | 'process.env': { 24 | NODE_ENV: '"production"', 25 | }, 26 | }), 27 | ], 28 | }); 29 | -------------------------------------------------------------------------------- /redux-elm-tomkis1/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2", "react"] 3 | } -------------------------------------------------------------------------------- /redux-elm-tomkis1/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /redux-elm-tomkis1/assets/waiting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slorber/scalable-frontend-with-elm-or-redux/f42646d2fc56949420eb802a85d851b0383f0069/redux-elm-tomkis1/assets/waiting.gif -------------------------------------------------------------------------------- /redux-elm-tomkis1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | redux-elm-skeleton 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /redux-elm-tomkis1/src/boilerplate.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { createStore, compose } from 'redux'; 4 | import { Provider, connect } from 'react-redux'; 5 | import reduxElm from 'redux-elm'; 6 | 7 | export default (containerDomId, View, updater) => { 8 | const storeFactory = compose( 9 | reduxElm, 10 | window.devToolsExtension ? window.devToolsExtension() : f => f 11 | )(createStore); 12 | 13 | const store = storeFactory(updater); 14 | 15 | const ConnectedView = connect(appState => ({ 16 | model: appState 17 | }))(View); 18 | 19 | render(( 20 | 21 | 22 | 23 | ), document.getElementById(containerDomId)); 24 | } -------------------------------------------------------------------------------- /redux-elm-tomkis1/src/button/updater.js: -------------------------------------------------------------------------------- 1 | import { Updater } from 'redux-elm'; 2 | 3 | export const isActive = model => model; 4 | 5 | export const initialModel = false; 6 | 7 | export default new Updater(initialModel) 8 | .case('Toggle', model => !model) 9 | .toReducer(); 10 | -------------------------------------------------------------------------------- /redux-elm-tomkis1/src/button/view.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { view } from 'redux-elm'; 3 | 4 | export default view(({ model, dispatch }) => 5 | ); 6 | -------------------------------------------------------------------------------- /redux-elm-tomkis1/src/counter/updater.js: -------------------------------------------------------------------------------- 1 | import { Updater } from 'redux-elm'; 2 | 3 | export const increment = model => model + 1; 4 | 5 | export const incrementByTwo = model => { 6 | if (model >= 10) { 7 | return model + 2; 8 | } else { 9 | return model + 1; 10 | } 11 | } 12 | 13 | export const initialModel = 0; 14 | 15 | export default new Updater(initialModel) 16 | .toReducer(); -------------------------------------------------------------------------------- /redux-elm-tomkis1/src/counter/view.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { view } from 'redux-elm'; 3 | 4 | export default view(({ model }) => 5 |
Value: {model}
); 6 | 7 | -------------------------------------------------------------------------------- /redux-elm-tomkis1/src/gif-viewer-pair-of-pairs/updater.js: -------------------------------------------------------------------------------- 1 | import { Updater } from 'redux-elm'; 2 | import { takeEvery } from 'redux-saga'; 3 | import { put } from 'redux-saga/effects'; 4 | 5 | import gifViewerUpdater, { initialModel as gifViewerInitialModel } from '../gif-viewer-pair/updater'; 6 | 7 | export const initialModel = { 8 | leftPair: gifViewerInitialModel, 9 | rightPair: gifViewerInitialModel 10 | }; 11 | 12 | export default new Updater(initialModel) 13 | .case('LeftPair', (model, action) => 14 | ({ ...model, leftPair: gifViewerUpdater(model.leftPair, action) })) 15 | .case('RightPair', (model, action) => 16 | ({ ...model, rightPair: gifViewerUpdater(model.rightPair, action) })) 17 | .toReducer(); 18 | -------------------------------------------------------------------------------- /redux-elm-tomkis1/src/gif-viewer-pair-of-pairs/view.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { view, forwardTo } from 'redux-elm'; 3 | 4 | import GifViewerPair from '../gif-viewer-pair/view'; 5 | 6 | export default view(({ model, dispatch }) => ( 7 |
8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 |
16 | )); -------------------------------------------------------------------------------- /redux-elm-tomkis1/src/gif-viewer-pair/updater.js: -------------------------------------------------------------------------------- 1 | import { Updater } from 'redux-elm'; 2 | import { takeEvery } from 'redux-saga'; 3 | import { put } from 'redux-saga/effects'; 4 | 5 | import gifViewerUpdater, { init as gifViewerInit } from '../gif-viewer/updater'; 6 | 7 | export const initialModel = { 8 | top: gifViewerInit('funny cats'), 9 | bottom: gifViewerInit('funny dogs') 10 | }; 11 | 12 | export default new Updater(initialModel) 13 | .case('Top', (model, action) => 14 | ({ ...model, top: gifViewerUpdater(model.top, action) })) 15 | .case('Bottom', (model, action) => 16 | ({ ...model, bottom: gifViewerUpdater(model.bottom, action) })) 17 | .toReducer(); -------------------------------------------------------------------------------- /redux-elm-tomkis1/src/gif-viewer-pair/view.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { forwardTo, view } from 'redux-elm'; 3 | 4 | import GifViewer from '../gif-viewer/view'; 5 | 6 | export default view(({ model, dispatch }) => ( 7 |
8 | 9 | 10 |
11 | )); 12 | -------------------------------------------------------------------------------- /redux-elm-tomkis1/src/gif-viewer/effects.js: -------------------------------------------------------------------------------- 1 | require('isomorphic-fetch'); 2 | 3 | export const fetchGif = topic => fetch(`http://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=${topic}`) 4 | .then(response => { 5 | if (response.status > 400) { 6 | throw new Error('Error while fetching from the server'); 7 | } else { 8 | return response.json(); 9 | } 10 | }) 11 | .then(body => body.data.image_url); 12 | -------------------------------------------------------------------------------- /redux-elm-tomkis1/src/gif-viewer/updater.js: -------------------------------------------------------------------------------- 1 | import { Updater } from 'redux-elm'; 2 | import { takeEvery } from 'redux-saga'; 3 | import { call, put, select } from 'redux-saga/effects'; 4 | 5 | import * as Effects from './effects'; 6 | 7 | const getTopic = model => model.topic; 8 | 9 | function* fetchGif() { 10 | const topic = yield select(getTopic); 11 | const url = yield call(Effects.fetchGif, topic); 12 | yield put({ type: 'NewGif', url }); 13 | } 14 | 15 | function* saga() { 16 | yield* fetchGif(); 17 | yield* takeEvery('RequestMore', fetchGif); 18 | } 19 | 20 | export const requestMore = () => ({ type: 'RequestMore' }); 21 | 22 | export const init = topic => ({ 23 | topic, 24 | gifUrl: null 25 | }); 26 | 27 | export default new Updater(init('funny cats'), saga) 28 | .case('NewGif', (model, { url }) => ({ ...model, gifUrl: url })) 29 | .case('RequestMore', model => ({ ...model, gifUrl: null })) 30 | .toReducer(); 31 | -------------------------------------------------------------------------------- /redux-elm-tomkis1/src/gif-viewer/view.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { view } from 'redux-elm'; 3 | 4 | const renderGif = url => { 5 | if (url) { 6 | return ; 7 | } else { 8 | return ; 9 | } 10 | } 11 | 12 | export default view(({ model, dispatch }) => ( 13 |
14 |

{model.topic}

15 | {renderGif(model.gifUrl)} 16 | 17 |
18 | )); 19 | -------------------------------------------------------------------------------- /redux-elm-tomkis1/src/main.js: -------------------------------------------------------------------------------- 1 | import run from './boilerplate'; 2 | 3 | import view from './root/view'; 4 | import updater from './root/updater'; 5 | 6 | run('app', view, updater); 7 | -------------------------------------------------------------------------------- /redux-elm-tomkis1/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | debug: true, 6 | target: 'web', 7 | devtool: 'sourcemap', 8 | plugins: [ 9 | new webpack.NoErrorsPlugin() 10 | ], 11 | entry: [ 12 | 'babel-polyfill', 13 | 'webpack-dev-server/client?http://localhost:3000', 14 | 'webpack/hot/only-dev-server', 15 | './src/main.js' 16 | ], 17 | output: { 18 | path: path.join(__dirname, './dev'), 19 | filename: 'app.bundle.js' 20 | }, 21 | module: { 22 | loaders: [{ 23 | test: /\.jsx$|\.js$/, 24 | loaders: ['babel-loader'], 25 | include: path.join(__dirname, './src') 26 | }] 27 | }, 28 | resolve: { 29 | extensions: ['', '.js', '.jsx'] 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /redux-fly-mrefrem/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | [*] 4 | # A special property that should be specified at the top of the file outside of 5 | # any sections. Set to true to stop .editor config file search on current file 6 | root = true 7 | 8 | # Indentation style 9 | # Possible values - tab, space 10 | indent_style = space 11 | 12 | # Indentation size in single-spaced characters 13 | # Possible values - an integer, tab 14 | indent_size = 2 15 | 16 | # Line ending file format 17 | # Possible values - lf, crlf, cr 18 | end_of_line = lf 19 | 20 | # File character encoding 21 | # Possible values - latin1, utf-8, utf-16be, utf-16le 22 | charset = utf-8 23 | 24 | # Denotes whether to trim whitespace at the end of lines 25 | # Possible values - true, false 26 | trim_trailing_whitespace = true 27 | 28 | # Denotes whether file should end with a newline 29 | # Possible values - true, false 30 | insert_final_newline = true 31 | -------------------------------------------------------------------------------- /redux-fly-mrefrem/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://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 15 | npm-debug.log 16 | -------------------------------------------------------------------------------- /redux-fly-mrefrem/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-fly-mrefrem", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "cpx": "^1.5.0", 7 | "enzyme": "^2.6.0", 8 | "gh-pages": "^0.12.0", 9 | "react-addons-test-utils": "^15.3.2", 10 | "react-scripts": "0.8.1", 11 | "rimraf": "^2.5.4" 12 | }, 13 | "dependencies": { 14 | "babel-polyfill": "^6.16.0", 15 | "react": "^15.3.2", 16 | "react-dom": "^15.3.2", 17 | "react-redux": "^4.4.5", 18 | "redbox-react": "^1.3.3", 19 | "redux": "^3.6.0", 20 | "redux-fly": "^0.3.0" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test --env=jsdom", 26 | "eject": "react-scripts eject" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /redux-fly-mrefrem/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slorber/scalable-frontend-with-elm-or-redux/f42646d2fc56949420eb802a85d851b0383f0069/redux-fly-mrefrem/public/favicon.ico -------------------------------------------------------------------------------- /redux-fly-mrefrem/src/Button.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { createReducer, getState } from 'redux-fly' 3 | 4 | const Button = ({ reduxState: { isActive }, reduxSetState }) => ( 5 | 11 | ) 12 | 13 | Button.propTypes = { 14 | reduxState: PropTypes.object.isRequired, 15 | reduxSetState: PropTypes.func.isRequired 16 | } 17 | 18 | export const checkIsActive = (mountPath, allState) => { 19 | const state = getState(mountPath)(allState) 20 | if (state) { 21 | return state.isActive 22 | } else { 23 | throw new Error(`Mounting path ${mountPath} isn't valid`) 24 | } 25 | } 26 | 27 | export default createReducer({ 28 | initialState: { 29 | isActive: false 30 | } 31 | })(Button) 32 | -------------------------------------------------------------------------------- /redux-fly-mrefrem/src/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { createReducer } from 'redux-fly' 3 | 4 | const Counter = ({ reduxState: { counter } }) => ( 5 |

Counter: {counter}

6 | ) 7 | 8 | Counter.propTypes = { 9 | reduxState: PropTypes.object.isRequired, 10 | buttonIsActive: PropTypes.func.isRequired, 11 | incrementAction: PropTypes.string.isRequired 12 | } 13 | 14 | export default createReducer({ 15 | initialState: { 16 | counter: 0 17 | }, 18 | listenActions: (state, action, props) => { 19 | if (action.type === props.incrementAction) { 20 | if (state.counter >= 10 && props.buttonIsActive()) { 21 | return { counter: state.counter + 2 } 22 | } 23 | return { counter: state.counter + 1 } 24 | } 25 | return state 26 | } 27 | })(Counter) 28 | -------------------------------------------------------------------------------- /redux-operations-DrorT/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-0"], 3 | "plugins": [ 4 | ["transform-decorators-legacy"], 5 | ["add-module-exports"] 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /redux-operations-DrorT/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .log 3 | .idea 4 | 5 | -------------------------------------------------------------------------------- /redux-operations-DrorT/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevTools } from 'redux-devtools'; 3 | import LogMonitor from 'redux-devtools-log-monitor'; 4 | import DockMonitor from 'redux-devtools-dock-monitor'; 5 | 6 | export default createDevTools( 7 | 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /redux-operations-DrorT/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose } from 'redux' 2 | import {reduxOperations} from 'redux-operations'; 3 | import rootReducer from './rootReducer' 4 | import DevTools from './DevTools'; 5 | 6 | 7 | const enhancer = compose( 8 | reduxOperations(), 9 | DevTools.instrument() 10 | ); 11 | 12 | export default function configureStore(initialState) { 13 | return createStore(rootReducer, initialState, enhancer); 14 | } 15 | -------------------------------------------------------------------------------- /redux-operations-DrorT/ducks/button.js: -------------------------------------------------------------------------------- 1 | import {INIT_REDUX_OPERATIONS} from 'redux-operations'; 2 | import {UPDATE_GIF} from './randomGif' 3 | export const CLICK_BUTTON = 'CLICK_BUTTON' 4 | 5 | export function clickButton(location, name) { 6 | return { 7 | type: CLICK_BUTTON, 8 | meta: {location, name} 9 | } 10 | } 11 | 12 | // state represent if button is clicked 13 | export const button = (state = false, action) => { 14 | if (action.type !== INIT_REDUX_OPERATIONS) return state; 15 | return { 16 | CLICK_BUTTON: { 17 | resolve: (state = false, action)=> !state 18 | }, 19 | UPDATE_GIF:{ 20 | priority: 5, 21 | resolve: (state = false, action)=> state 22 | }, 23 | signature: '@@reduxOperations' 24 | } 25 | }; 26 | 27 | -------------------------------------------------------------------------------- /redux-operations-DrorT/ducks/gifCounter.js: -------------------------------------------------------------------------------- 1 | import {INIT_REDUX_OPERATIONS} from 'redux-operations'; 2 | import {UPDATE_GIF} from './randomGif' 3 | 4 | export const gifCounter = (state = {counter:0, button:false}, action) => { 5 | if (action.type !== INIT_REDUX_OPERATIONS) return state; 6 | return { 7 | UPDATE_GIF: { 8 | priority: 10, 9 | resolve: (state = {counter:0, button:false}, action)=> { 10 | if ( ( state.counter >= 10 ) && ( state.button ) ) { 11 | return {...state, counter: state.counter+2}; 12 | } 13 | else { 14 | return {...state, counter: state.counter+1}; 15 | } 16 | } 17 | }, 18 | signature: '@@reduxOperations' 19 | } 20 | }; 21 | 22 | -------------------------------------------------------------------------------- /redux-operations-DrorT/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux counter example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /redux-operations-DrorT/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | import Counters from './containers/App' 5 | import configureStore from './configureStore' 6 | import DevTools from './DevTools'; 7 | 8 | const store = configureStore(); 9 | 10 | render( 11 | 12 |
13 | 14 | 15 |
16 |
, 17 | document.getElementById('root') 18 | ) 19 | -------------------------------------------------------------------------------- /redux-operations-DrorT/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import {counter} from './ducks/counter' 3 | import {button} from './ducks/button' 4 | import {randomGif} from './ducks/randomGif' 5 | import {gifCounter} from './ducks/gifCounter' 6 | 7 | export default combineReducers({ 8 | counter, 9 | button, 10 | randomGif, 11 | gifCounter 12 | }); 13 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2", "react"] 3 | } -------------------------------------------------------------------------------- /redux-saga-jaysoo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demo App 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/counter/Container.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { bindActionCreators } from 'redux' 3 | import { createStructuredSelector } from 'reselect' 4 | import { connect } from 'react-redux' 5 | 6 | import { getModel } from './selectors' 7 | import * as actions from './actions' 8 | 9 | export const Component = ({ model, inc, dec }) => ( 10 |
11 | 14 | 15 | {model} 16 | 17 | 20 |
21 | ) 22 | 23 | export default connect( 24 | createStructuredSelector({ 25 | model: getModel 26 | }), 27 | dispatch => bindActionCreators(actions, dispatch) 28 | )(Component) 29 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/counter/__init__.js: -------------------------------------------------------------------------------- 1 | export const name = 'counter' 2 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/counter/actions.js: -------------------------------------------------------------------------------- 1 | import { kAction } from '../utils' 2 | import { name } from './__init__' 3 | 4 | export const INC = `${name}/INC` 5 | export const DEC = `${name}/DEC` 6 | 7 | export const inc = kAction(INC) 8 | 9 | export const dec = kAction(DEC) 10 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/counter/index.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions' 2 | import reducer, { initialState } from './reducer' 3 | import * as selectors from './selectors' 4 | import Container, { Component } from './Container' 5 | import Model from './model' 6 | import { name } from './__init__' 7 | export 8 | { name 9 | , actions 10 | , initialState 11 | , reducer 12 | , selectors 13 | , Model 14 | , Container 15 | , Component 16 | } 17 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/counter/model.js: -------------------------------------------------------------------------------- 1 | export default Number 2 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/counter/reducer.js: -------------------------------------------------------------------------------- 1 | import { INC, DEC } from './actions' 2 | import Model from './model' 3 | 4 | export const initialState = Model(0) 5 | 6 | export default (state = initialState, action) => { 7 | switch (action.type) { 8 | case INC: 9 | return state + Model(1) 10 | case DEC: 11 | return state - Model(1) 12 | default: 13 | return state 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/counter/selectors.js: -------------------------------------------------------------------------------- 1 | import { prop } from 'ramda' 2 | import { name } from './__init__' 3 | 4 | // getModel :: State -> Model 5 | export const getModel = prop(name) 6 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/index.js: -------------------------------------------------------------------------------- 1 | import './styles.css' 2 | import 'babel-polyfill' 3 | import React from 'react' 4 | import { render } from 'react-dom' 5 | import { Provider } from 'react-redux' 6 | import * as main from './main' 7 | import { createStore, applyMiddleware } from 'redux' 8 | import createSagaMiddleware from 'redux-saga' 9 | 10 | const sagaMiddleware = createSagaMiddleware(main.saga) 11 | const store = createStore(main.reducer, applyMiddleware(sagaMiddleware)) 12 | 13 | render( 14 | 15 | 16 | 17 | , 18 | document.getElementById('app') 19 | ) -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/randomGif/__init__.js: -------------------------------------------------------------------------------- 1 | export const name = 'randomGif' 2 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/randomGif/actions.js: -------------------------------------------------------------------------------- 1 | import Task from 'data.task'; 2 | import { kAction } from '../utils' 3 | import { name } from './__init__' 4 | 5 | export const NEW_GIF = `${name}/NEW_GIF` 6 | export const REQUEST_MORE = `${name}/REQUEST_MORE` 7 | export const PENDING = `${name}/PENDING` 8 | 9 | export const newGif = url => ({ type: NEW_GIF, payload: url }) 10 | 11 | export const requestMore = topic => ({ type: REQUEST_MORE, payload: topic }) 12 | 13 | export const pending = kAction(PENDING) 14 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/randomGif/index.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions' 2 | import reducer, { initialState } from './reducer' 3 | import saga from './saga' 4 | import * as selectors from './selectors' 5 | import * as tasks from './tasks' 6 | import Model from './model' 7 | import Container, { Component } from './Container' 8 | import { name } from './__init__' 9 | export 10 | { name 11 | , actions 12 | , initialState 13 | , reducer 14 | , saga 15 | , selectors 16 | , tasks 17 | , Model 18 | , Container 19 | , Component 20 | } 21 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/randomGif/model.js: -------------------------------------------------------------------------------- 1 | import daggy from 'daggy' 2 | 3 | export default daggy.taggedSum({ 4 | Empty: ['topic'], 5 | Pending: ['topic'], 6 | Loaded: ['topic', 'url'] 7 | }) 8 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/randomGif/reducer.js: -------------------------------------------------------------------------------- 1 | import { NEW_GIF, PENDING } from './actions' 2 | import Model from './model' 3 | 4 | export const initialState = Model.Empty('cats') 5 | 6 | export default (state = initialState, action) => { 7 | switch (action.type) { 8 | case NEW_GIF: 9 | return Model.Loaded(state.topic, action.payload) 10 | case PENDING: 11 | return Model.Pending(state.topic) 12 | default: 13 | return state 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/randomGif/saga.js: -------------------------------------------------------------------------------- 1 | import { call, fork, take, put } from 'redux-saga/effects' 2 | import * as tasks from '../tasks' 3 | import * as actions from './actions' 4 | import { fetchRandomGif } from './tasks' 5 | 6 | function* watchRequestMore() { 7 | while (true) { 8 | const { payload: topic } = yield take(actions.REQUEST_MORE) 9 | yield put(actions.pending()) 10 | yield put(tasks.actions.runTask(fetchRandomGif(topic), actions.NEW_GIF, actions.NEW_GIF)) 11 | } 12 | } 13 | 14 | export default function* () { 15 | yield [ fork(watchRequestMore) ] 16 | } -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/randomGif/selectors.js: -------------------------------------------------------------------------------- 1 | import { compose, prop } from 'ramda' 2 | import { name } from './__init__' 3 | 4 | // getModel :: State -> Model 5 | export const getModel = prop(name) 6 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/randomGifList/__init__.js: -------------------------------------------------------------------------------- 1 | export const name = 'randomGifList' 2 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/randomGifList/actions.js: -------------------------------------------------------------------------------- 1 | import { kAction } from '../utils' 2 | import { name } from './__init__' 3 | 4 | export const ADD = `${name}/ADD` 5 | export const CHANGE_TOPIC = `${name}/CHANGE_TOPIC` 6 | export const NEW_GIF = `${name}/NEW_GIF` 7 | export const REQUEST_MORE = `${name}/REQUEST_MORE` 8 | export const PENDING = `${name}/PENDING` 9 | 10 | export const add = topic => ({ type: ADD, payload: { topic }}) 11 | 12 | export const changeTopic = topic => ({ type: CHANGE_TOPIC, payload: { topic } }) 13 | 14 | export const newGif = ({ id, url }) => ({ type: NEW_GIF, payload: { id, url } }) 15 | 16 | export const requestMore = ({ id, topic }) => ({ type: REQUEST_MORE, payload: { id, topic } }) 17 | 18 | export const pending = id => ({ type: PENDING, payload: { id } }) 19 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/randomGifList/index.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions' 2 | import reducer, { initialState } from './reducer' 3 | import * as selectors from './selectors' 4 | import saga from './saga' 5 | import Container, { Component } from './Container' 6 | import { name } from './__init__' 7 | export 8 | { name 9 | , actions 10 | , initialState 11 | , reducer 12 | , selectors 13 | , saga 14 | , Container 15 | , Component 16 | } 17 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/randomGifList/saga.js: -------------------------------------------------------------------------------- 1 | import { call, fork, take, put } from 'redux-saga/effects' 2 | import { compose, lift } from 'ramda' 3 | import * as randomGif from '../randomGif' 4 | import * as tasks from '../tasks' 5 | import * as actions from './actions' 6 | 7 | const withID = id => url => ({ url, id }) 8 | 9 | function* watchRequestMore() { 10 | while (true) { 11 | const { payload: { id, topic } } = yield take(actions.REQUEST_MORE) 12 | yield put(actions.pending(id)) 13 | const task = compose(lift(withID(id)), randomGif.tasks.fetchRandomGif)(topic) 14 | yield put(tasks.actions.runTask(task, actions.NEW_GIF, actions.NEW_GIF)) 15 | } 16 | } 17 | 18 | export default function* () { 19 | yield [ fork(watchRequestMore) ] 20 | } 21 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/randomGifList/selectors.js: -------------------------------------------------------------------------------- 1 | import { compose, prop } from 'ramda' 2 | import * as randomGif from '../randomGif' 3 | import { name } from './__init__' 4 | 5 | // getModel :: State -> Model 6 | export const getModel = prop(name) 7 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/randomGifPair/__init__.js: -------------------------------------------------------------------------------- 1 | export const name = 'randomGifPair' 2 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/randomGifPair/actions.js: -------------------------------------------------------------------------------- 1 | import { kAction } from '../utils' 2 | import { name } from './__init__' 3 | 4 | export const NEW_GIF = `${name}/NEW_GIF` 5 | export const REQUEST_MORE = `${name}/REQUEST_MORE` 6 | export const PENDING = `${name}/PENDING` 7 | 8 | export const newGif = ({side, url}) => ({ type: NEW_GIF, payload: {side, url} }) 9 | 10 | export const requestMore = ({side, topic}) => ({ type: REQUEST_MORE, payload: {side, topic} }) 11 | 12 | export const pending = ({side}) => ({ type: PENDING, payload: {side} }) 13 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/randomGifPair/index.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions' 2 | import reducer, { initialState } from './reducer' 3 | import * as selectors from './selectors' 4 | import saga from './saga' 5 | import Container, { Component } from './Container' 6 | import { name } from './__init__' 7 | export 8 | { name 9 | , actions 10 | , initialState 11 | , reducer 12 | , selectors 13 | , saga 14 | , Container 15 | , Component 16 | } 17 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/randomGifPair/saga.js: -------------------------------------------------------------------------------- 1 | import { call, fork, take, put } from 'redux-saga/effects' 2 | import { compose, lift } from 'ramda' 3 | import * as randomGif from '../randomGif' 4 | import * as tasks from '../tasks' 5 | import * as actions from './actions' 6 | 7 | const withSide = side => url => ({ url, side }) 8 | 9 | function* doRequestMore(side, topic) { 10 | yield put(actions.pending({ side })) 11 | const task = compose(lift(withSide(side)), randomGif.tasks.fetchRandomGif)(topic) 12 | yield put(tasks.actions.runTask(task, actions.NEW_GIF, actions.NEW_GIF)) 13 | } 14 | 15 | function* watchRequestMore() { 16 | while (true) { 17 | const { payload: { side, topic } } = yield take(actions.REQUEST_MORE) 18 | yield fork(doRequestMore, side, topic) 19 | } 20 | } 21 | 22 | export default function* () { 23 | yield [ fork(watchRequestMore) ] 24 | } 25 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/randomGifPair/selectors.js: -------------------------------------------------------------------------------- 1 | import { compose, prop } from 'ramda' 2 | import * as randomGif from '../randomGif' 3 | import { name } from './__init__' 4 | 5 | // getModel :: State -> Model 6 | export const getModel = prop(name) 7 | 8 | export const getLeft = compose(randomGif.selectors.getModel, prop('left'), getModel) 9 | 10 | export const getRight = compose(randomGif.selectors.getModel, prop('right'), getModel) -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/randomGifPairOfPair/__init__.js: -------------------------------------------------------------------------------- 1 | export const name = 'randomGifPairOfPair' 2 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/randomGifPairOfPair/actions.js: -------------------------------------------------------------------------------- 1 | import { kAction } from '../utils' 2 | import { name } from './__init__' 3 | 4 | export const NEW_GIF = `${name}/NEW_GIF` 5 | export const REQUEST_MORE = `${name}/REQUEST_MORE` 6 | export const PENDING = `${name}/PENDING` 7 | 8 | export const newGif = ({position, topic}) => ({ type: NEW_GIF, payload: {position, topic} }) 9 | 10 | export const requestMore = ({position, topic}) => ({ type: REQUEST_MORE, payload: {position, topic} }) 11 | 12 | export const pending = ({position}) => ({ type: PENDING, payload: {position} }) 13 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/randomGifPairOfPair/index.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions' 2 | import reducer, { initialState } from './reducer' 3 | import * as selectors from './selectors' 4 | import saga from './saga' 5 | import Container, { Component } from './Container' 6 | import { name } from './__init__' 7 | export 8 | { name 9 | , actions 10 | , initialState 11 | , reducer 12 | , selectors 13 | , saga 14 | , Container 15 | , Component 16 | } 17 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/randomGifPairOfPair/saga.js: -------------------------------------------------------------------------------- 1 | import { call, fork, take, put } from 'redux-saga/effects' 2 | import { compose, lift } from 'ramda' 3 | import * as randomGif from '../randomGif' 4 | import * as tasks from '../tasks' 5 | import * as actions from './actions' 6 | 7 | const withPosition = position => url => ({ url, position }) 8 | 9 | function* doRequestMore(position, topic) { 10 | yield put(actions.pending({ position })) 11 | const task = compose(lift(withPosition(position)), randomGif.tasks.fetchRandomGif)(topic) 12 | yield put(tasks.actions.runTask(task, actions.NEW_GIF, actions.NEW_GIF)) 13 | } 14 | 15 | function* watchRequestMore() { 16 | while (true) { 17 | const { payload: { position, topic } } = yield take(actions.REQUEST_MORE) 18 | yield fork(doRequestMore, position, topic) 19 | } 20 | } 21 | 22 | export default function* () { 23 | yield [ fork(watchRequestMore) ] 24 | } 25 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/randomGifPairOfPair/selectors.js: -------------------------------------------------------------------------------- 1 | import { compose, prop } from 'ramda' 2 | import * as randomGifPair from '../randomGifPair' 3 | import { name } from './__init__' 4 | 5 | // getModel :: State -> Model 6 | export const getModel = prop(name) 7 | 8 | export const getTopLeft = compose(randomGifPair.selectors.getLeft, prop('top'), getModel) 9 | 10 | export const getTopRight = compose(randomGifPair.selectors.getRight, prop('top'), getModel) 11 | 12 | export const getBottomLeft = compose(randomGifPair.selectors.getLeft, prop('bottom'), getModel) 13 | 14 | export const getBottomRight = compose(randomGifPair.selectors.getRight, prop('bottom'), getModel) -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif 3 | } 4 | 5 | .container { 6 | margin-bottom: 20px 7 | } 8 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/tasks/actions.js: -------------------------------------------------------------------------------- 1 | export const RUN_TASK = 'RUN_TASK' 2 | 3 | export const runTask = (task, successType, failureType) => ({ 4 | type: RUN_TASK, 5 | task, 6 | successType, 7 | failureType 8 | }) 9 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/tasks/index.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions' 2 | import saga from './saga' 3 | 4 | export 5 | { actions 6 | , saga 7 | } 8 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/tasks/saga.js: -------------------------------------------------------------------------------- 1 | import { call, fork, put, take } from 'redux-saga/effects' 2 | import { RUN_TASK } from './actions' 3 | 4 | const runTask = (task) => ( 5 | new Promise((res) => { 6 | task.fork( 7 | x => res({ rejected: x }) 8 | , 9 | x => res({ resolved: x }) 10 | ) 11 | }) 12 | ) 13 | 14 | function* doRunTask(task, successType, failureType) { 15 | const { resolved, rejected } = yield call(runTask, task) 16 | if (resolved) { 17 | yield put ({ type: successType, payload : resolved }) 18 | } else { 19 | yield put ({ type: failureType, payload : rejected }) 20 | } 21 | } 22 | function* watchRunTasks() { 23 | while (true) { 24 | const { task, successType, failureType } = yield take(RUN_TASK) 25 | yield fork(doRunTask, task, successType, failureType) 26 | } 27 | } 28 | 29 | export default function* () { 30 | yield [ fork(watchRunTasks) ] 31 | } 32 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/theButton/Container.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { bindActionCreators } from 'redux' 3 | import { createStructuredSelector } from 'reselect' 4 | import { connect } from 'react-redux' 5 | 6 | import { getModel } from './selectors' 7 | import * as actions from './actions' 8 | 9 | export const Component = ({ model, turnOn, turnOff }) => { 10 | return ( 11 | 15 | ) 16 | } 17 | 18 | export default connect( 19 | createStructuredSelector({ 20 | model: getModel 21 | }), 22 | dispatch => bindActionCreators(actions, dispatch) 23 | )(Component) 24 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/theButton/__init__.js: -------------------------------------------------------------------------------- 1 | export const name = 'theButton' 2 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/theButton/actions.js: -------------------------------------------------------------------------------- 1 | import { kAction } from '../utils' 2 | import { name } from './__init__' 3 | 4 | export const TURN_ON = `${name}/TURN_ON` 5 | export const TURN_OFF = `${name}/TURN_OFF` 6 | 7 | export const turnOn = kAction(TURN_ON) 8 | export const turnOff = kAction(TURN_OFF) 9 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/theButton/index.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions' 2 | import reducer, { initialState } from './reducer' 3 | import * as selectors from './selectors' 4 | import Container, { Component } from './Container' 5 | import Model from './model' 6 | import { name } from './__init__' 7 | export 8 | { name 9 | , actions 10 | , initialState 11 | , reducer 12 | , selectors 13 | , Model 14 | , Container 15 | , Component 16 | } 17 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/theButton/model.js: -------------------------------------------------------------------------------- 1 | export default Boolean 2 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/theButton/reducer.js: -------------------------------------------------------------------------------- 1 | import { TURN_ON, TURN_OFF } from './actions' 2 | import Model from './model' 3 | 4 | export const initialState = false 5 | 6 | export default (state = initialState, action) => { 7 | switch (action.type) { 8 | case TURN_ON: 9 | return true 10 | case TURN_OFF: 11 | return false 12 | default: 13 | return state 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/theButton/selectors.js: -------------------------------------------------------------------------------- 1 | import { prop } from 'ramda' 2 | import { name } from './__init__' 3 | 4 | // getModel :: State -> Model 5 | export const getModel = prop(name) 6 | -------------------------------------------------------------------------------- /redux-saga-jaysoo/src/utils.js: -------------------------------------------------------------------------------- 1 | import { always as k } from 'ramda' 2 | 3 | export const kAction = type => k({ type }) 4 | -------------------------------------------------------------------------------- /redux-serial-effects/.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 | -------------------------------------------------------------------------------- /redux-serial-effects/config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); 17 | -------------------------------------------------------------------------------- /redux-serial-effects/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slorber/scalable-frontend-with-elm-or-redux/f42646d2fc56949420eb802a85d851b0383f0069/redux-serial-effects/public/favicon.ico -------------------------------------------------------------------------------- /redux-serial-effects/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 | -------------------------------------------------------------------------------- /redux-serial-effects/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-intro { 18 | font-size: large; 19 | } 20 | 21 | @keyframes App-logo-spin { 22 | from { transform: rotate(0deg); } 23 | to { transform: rotate(360deg); } 24 | } 25 | -------------------------------------------------------------------------------- /redux-serial-effects/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /redux-serial-effects/src/button/Container.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { bindActionCreators } from 'redux' 3 | import { connect } from 'react-redux' 4 | import * as actions from './actions' 5 | 6 | export const Component = ({ isEnabled, flip }) => ( 7 |
8 | 14 |
15 | ) 16 | 17 | export default selector => 18 | connect( 19 | state => ({ isEnabled: state[selector] }), 20 | dispatch => bindActionCreators(actions, dispatch) 21 | )(Component) 22 | -------------------------------------------------------------------------------- /redux-serial-effects/src/button/actionTypes.js: -------------------------------------------------------------------------------- 1 | const FLIP = 'FLIP' 2 | 3 | export { FLIP } 4 | -------------------------------------------------------------------------------- /redux-serial-effects/src/button/actions.js: -------------------------------------------------------------------------------- 1 | import { FLIP } from './actionTypes' 2 | 3 | function flip() { 4 | return { type: FLIP } 5 | } 6 | 7 | export { flip } 8 | -------------------------------------------------------------------------------- /redux-serial-effects/src/button/reducer.js: -------------------------------------------------------------------------------- 1 | import { FLIP } from './actionTypes' 2 | 3 | function reducer(state = false, action) { 4 | switch (action.type) { 5 | case FLIP: 6 | return !state 7 | default: 8 | return state 9 | } 10 | } 11 | 12 | const getEnabledState = state => state 13 | 14 | export default reducer 15 | export { getEnabledState } 16 | -------------------------------------------------------------------------------- /redux-serial-effects/src/counter/Container.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { bindActionCreators } from 'redux' 3 | import { connect } from 'react-redux' 4 | import * as actions from './actions' 5 | 6 | export const Component = ({ counterValue, inc, dec }) => ( 7 |
8 | 11 | 12 | {counterValue} 13 | 14 | 17 |
18 | ) 19 | 20 | export default selector => 21 | connect( 22 | state => ({ counterValue: state[selector] }), 23 | dispatch => bindActionCreators(actions, dispatch) 24 | )(Component) 25 | -------------------------------------------------------------------------------- /redux-serial-effects/src/counter/actionTypes.js: -------------------------------------------------------------------------------- 1 | const INC = 'INC' 2 | const DEC = 'DEC' 3 | 4 | export { INC, DEC } 5 | -------------------------------------------------------------------------------- /redux-serial-effects/src/counter/actions.js: -------------------------------------------------------------------------------- 1 | import { INC, DEC } from './actionTypes' 2 | 3 | function inc(by) { 4 | return { type: INC, by } 5 | } 6 | 7 | function dec(by) { 8 | return { type: DEC, by } 9 | } 10 | 11 | export { inc, dec } 12 | -------------------------------------------------------------------------------- /redux-serial-effects/src/counter/reducer.js: -------------------------------------------------------------------------------- 1 | import { INC, DEC } from './actionTypes' 2 | 3 | function reducer(state = 0, action) { 4 | switch (action.type) { 5 | case INC: 6 | return state + action.by 7 | case DEC: 8 | return state > action.by ? state - action.by : 0 9 | default: 10 | return state 11 | } 12 | } 13 | 14 | const getValue = state => state 15 | 16 | export default reducer 17 | export { getValue } 18 | -------------------------------------------------------------------------------- /redux-serial-effects/src/gif/actionTypes.js: -------------------------------------------------------------------------------- 1 | const NEW_GIF = 'NEW_GIF' 2 | const UPDATED = 'UPDATED' 3 | 4 | export { NEW_GIF, UPDATED } 5 | -------------------------------------------------------------------------------- /redux-serial-effects/src/gif/actions.js: -------------------------------------------------------------------------------- 1 | import { NEW_GIF, UPDATED } from './actionTypes' 2 | 3 | const newGif = topic => ({ type: NEW_GIF, topic }) 4 | 5 | const updated = url => ({ type: UPDATED, url }) 6 | 7 | export { newGif, updated } 8 | -------------------------------------------------------------------------------- /redux-serial-effects/src/gif/reducer.js: -------------------------------------------------------------------------------- 1 | import { NEW_GIF, UPDATED } from './actionTypes' 2 | import states from './states' 3 | 4 | const reducer = ( 5 | state = { 6 | state: 'INIT', 7 | topic: '', 8 | url: '' 9 | }, 10 | action 11 | ) => { 12 | switch (action.type) { 13 | case NEW_GIF: 14 | return Object.assign({}, state, { 15 | state: states.request, 16 | topic: action.topic 17 | }) 18 | case UPDATED: 19 | return Object.assign({}, state, { state: states.ready, url: action.url }) 20 | default: 21 | return state 22 | } 23 | } 24 | 25 | const getState = state => state.state 26 | const isRequesting = state => state.state === states.request 27 | 28 | export default reducer 29 | export { getState, isRequesting } 30 | -------------------------------------------------------------------------------- /redux-serial-effects/src/gif/states.js: -------------------------------------------------------------------------------- 1 | const states = { 2 | init: 'INIT', 3 | ready: 'READY', 4 | request: 'REQUEST' 5 | } 6 | 7 | export default states 8 | -------------------------------------------------------------------------------- /redux-serial-effects/src/gif/subscriber.js: -------------------------------------------------------------------------------- 1 | import states from './states' 2 | import { updated } from './actions' 3 | 4 | const subscriber = ({ from, to }, dispatch) => { 5 | if (to.state !== from.state && to.state === states.request) { 6 | fetch( 7 | `https://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&topic=${to.topic}` 8 | ) 9 | .then(response => response.json()) 10 | .then(json => { 11 | dispatch(updated(json.data.image_url)) 12 | }) 13 | } 14 | } 15 | 16 | export default subscriber 17 | -------------------------------------------------------------------------------- /redux-serial-effects/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /redux-serial-effects/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './index.css' 4 | import App from './App' 5 | import registerServiceWorker from './registerServiceWorker' 6 | 7 | ReactDOM.render(, document.getElementById('root')) 8 | registerServiceWorker() 9 | -------------------------------------------------------------------------------- /redux-serial-effects/src/main/mountPoints.js: -------------------------------------------------------------------------------- 1 | export default { 2 | button: 'button', 3 | counter: 'counter', 4 | randomGif: 'randomGif', 5 | randomGifPair: 'randomGifPair', 6 | randomGifPairOfPair: 'randomGifPairOfPair' 7 | } 8 | -------------------------------------------------------------------------------- /redux-serial-effects/src/main/rootSubscriber.js: -------------------------------------------------------------------------------- 1 | import { combineSubscribers } from 'redux-serial-effects' 2 | import relocatableGif from '../relocatableGif/subscriber' 3 | import randomGifPair from '../randomGifPair/subscriber' 4 | import randomGifPairOfPair from '../randomGifPairOfPair/subscriber' 5 | import mountPoints from './mountPoints' 6 | 7 | const randomGif = relocatableGif(mountPoints.randomGif, 1) 8 | const randomGifPairSubscriber = randomGifPair(mountPoints.randomGifPair) 9 | const randomGifPairOfPairSubscriber = randomGifPairOfPair( 10 | mountPoints.randomGifPairOfPair 11 | ) 12 | 13 | const rootSubscriber = combineSubscribers({ 14 | [mountPoints.randomGif]: randomGif, 15 | [mountPoints.randomGifPair]: randomGifPairSubscriber, 16 | [mountPoints.randomGifPairOfPair]: randomGifPairOfPairSubscriber 17 | }) 18 | 19 | export default rootSubscriber 20 | -------------------------------------------------------------------------------- /redux-serial-effects/src/randomGifPair/Container.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import relocatableGif from '../relocatableGif/Container' 3 | 4 | const Container = mountPoint => ({ topics, onNewRequested }) => { 5 | const RandomGifPair = relocatableGif(mountPoint, 2) 6 | 7 | return ( 8 |
9 |
10 | 11 |
12 |
13 | ) 14 | } 15 | 16 | export default Container 17 | -------------------------------------------------------------------------------- /redux-serial-effects/src/randomGifPair/reducer.js: -------------------------------------------------------------------------------- 1 | import relocatableGif from '../relocatableGif/reducer' 2 | 3 | export default mountPoint => { 4 | const randomGifPair = relocatableGif(mountPoint, 2) 5 | 6 | return { 7 | reducer: randomGifPair.reducer, 8 | selectors: { 9 | getFirstGifState: (state, index) => 10 | randomGifPair.selectors.getState(state.randomGifPair, 0), 11 | getSecondGifState: (state, index) => 12 | randomGifPair.selectors.getState(state.randomGifPair, 1), 13 | isFirstGifRequesting: (state, index) => 14 | randomGifPair.selectors.isRequesting(state.randomGifPair, 0), 15 | isSecondGifRequesting: (state, index) => 16 | randomGifPair.selectors.isRequesting(state.randomGifPair, 1) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /redux-serial-effects/src/randomGifPair/subscriber.js: -------------------------------------------------------------------------------- 1 | import relocatableGif from '../relocatableGif/subscriber' 2 | 3 | export default mountPoint => { 4 | const randomGifPair = relocatableGif(mountPoint, 2) 5 | 6 | return randomGifPair 7 | } 8 | -------------------------------------------------------------------------------- /redux-serial-effects/src/randomGifPairOfPair/Container.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import relocatableGif from '../relocatableGif/Container' 3 | 4 | const Container = mountPoint => ({ topics, onNewRequested }) => { 5 | const RandomGifPairOne = relocatableGif(mountPoint, [0, 1]) 6 | const RandomGifPairTwo = relocatableGif(mountPoint, [2, 3]) 7 | 8 | return ( 9 |
10 |
11 | 15 |
16 |
17 | 21 |
22 |
23 | ) 24 | } 25 | 26 | export default Container 27 | -------------------------------------------------------------------------------- /redux-serial-effects/src/randomGifPairOfPair/subscriber.js: -------------------------------------------------------------------------------- 1 | import relocatableGif from '../relocatableGif/subscriber' 2 | 3 | export default mountPoint => { 4 | const randomGifPairOfPair = relocatableGif(mountPoint, 4) 5 | 6 | return randomGifPairOfPair 7 | } 8 | -------------------------------------------------------------------------------- /redux-serial-effects/src/relocatableGif/Container.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import gif from '../gif/Container' 3 | import encapsulate from '../encapsulateComponent' 4 | 5 | const RelocatableGif = (mountPoint, times) => ({ topics, onNewRequested }) => { 6 | const encapsulated = encapsulate(times, mountPoint) 7 | 8 | return ( 9 |
10 | {encapsulated.forEach(x => { 11 | const Gif = encapsulated.component(gif, x) 12 | return ( 13 |
14 | 15 |
16 | ) 17 | })} 18 |
19 | ) 20 | } 21 | 22 | export default RelocatableGif 23 | -------------------------------------------------------------------------------- /redux-serial-effects/src/relocatableGif/reducer.js: -------------------------------------------------------------------------------- 1 | import gifReducer, { 2 | getState as gifGetState, 3 | isRequesting as gifIsRequesting 4 | } from '../gif/reducer' 5 | 6 | import encapsulate from '../encapsulateComponent' 7 | 8 | export default (mountPoint, times) => { 9 | const encapsulated = encapsulate(times, mountPoint) 10 | 11 | return { 12 | reducer: encapsulated.reducer(gifReducer), 13 | selectors: { 14 | getState: (state, index) => gifGetState(state[index]), 15 | isRequesting: (state, index) => gifIsRequesting(state[index]) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /redux-serial-effects/src/relocatableGif/subscriber.js: -------------------------------------------------------------------------------- 1 | import encapsulate from '../encapsulateComponent' 2 | import gifSubscriber from '../gif/subscriber' 3 | 4 | export default (mountPoint, times) => { 5 | const encapsulated = encapsulate(times, mountPoint) 6 | return encapsulated.subscriber(gifSubscriber) 7 | } 8 | -------------------------------------------------------------------------------- /redux-ship-clarus/.flowconfig: -------------------------------------------------------------------------------- 1 | 2 | [ignore] 3 | /node_modules/fbjs/.* 4 | 5 | [include] 6 | 7 | [libs] 8 | decls 9 | 10 | [options] 11 | -------------------------------------------------------------------------------- /redux-ship-clarus/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://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 15 | npm-debug.log 16 | -------------------------------------------------------------------------------- /redux-ship-clarus/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-ship-clarus", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "http://clarus.github.io/redux-ship/examples/scalable-frontend-with-elm-or-redux", 6 | "devDependencies": { 7 | "react-scripts": "0.6.1" 8 | }, 9 | "dependencies": { 10 | "babel-polyfill": "^6.16.0", 11 | "react": "^15.3.2", 12 | "react-dom": "^15.3.2", 13 | "react-test-renderer": "^15.3.2", 14 | "redux": "^3.6.0", 15 | "redux-ship": "0.0.13", 16 | "redux-ship-logger": "0.0.9" 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 | } 24 | } 25 | -------------------------------------------------------------------------------- /redux-ship-clarus/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slorber/scalable-frontend-with-elm-or-redux/f42646d2fc56949420eb802a85d851b0383f0069/redux-ship-clarus/public/favicon.ico -------------------------------------------------------------------------------- /redux-ship-clarus/src/__tests__/model.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as Test from '../test'; 3 | import * as Model from '../model'; 4 | 5 | Test.snapshotReduce(Model.reduce, { 6 | button: { 7 | patch: {button: {type: 'Toggle'}}, 8 | state: Model.initialState, 9 | }, 10 | counter: { 11 | patch: {counter: {type: 'IncrementByOne'}}, 12 | state: Model.initialState, 13 | }, 14 | randomGif: { 15 | patch: {randomGif: {type: 'LoadStart'}}, 16 | state: Model.initialState, 17 | }, 18 | randomGifPair: { 19 | patch: {randomGifPair: {first: {type: 'LoadStart'}}}, 20 | state: Model.initialState, 21 | }, 22 | randomGifPairPair: { 23 | patch: {randomGifPairPair: {first: {second: {type: 'LoadStart'}}}}, 24 | state: Model.initialState, 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/__tests__/view.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import * as Test from '../test'; 4 | import * as Model from '../model'; 5 | import Index from '../view'; 6 | 7 | const defaultProps = { 8 | dispatch: Test.dispatch, 9 | state: Model.initialState, 10 | }; 11 | 12 | Test.snapshotComponent(Index, { 13 | 'default': defaultProps, 14 | }); 15 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/button/__tests__/__snapshots__/model.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`test activate 1`] = ` 2 | Object { 3 | "status": "green" 4 | } 5 | `; 6 | 7 | exports[`test desactivate 1`] = ` 8 | Object { 9 | "status": "red" 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/button/__tests__/__snapshots__/view.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`test default 1`] = ` 2 |
3 | 12 |
13 | `; 14 | 15 | exports[`test disabled 1`] = ` 16 |
17 | 26 |
27 | `; 28 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/button/__tests__/model.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as Test from '../../test'; 3 | import * as ButtonModel from '../model'; 4 | 5 | Test.snapshotReduce(ButtonModel.reduce, { 6 | activate: { 7 | patch: {type: 'Toggle'}, 8 | state: { 9 | ...ButtonModel.initialState, 10 | status: 'red', 11 | }, 12 | }, 13 | desactivate: { 14 | patch: {type: 'Toggle'}, 15 | state: ButtonModel.initialState, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/button/__tests__/view.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import * as Test from '../../test'; 4 | import * as ButtonModel from '../model'; 5 | import Button from '../view'; 6 | 7 | const defaultProps = { 8 | dispatch: Test.dispatch, 9 | state: ButtonModel.initialState, 10 | }; 11 | 12 | Test.snapshotComponent(Button, { 13 | 'default': defaultProps, 14 | disabled: { 15 | ...defaultProps, 16 | state: { 17 | ...defaultProps.state, 18 | status: 'red', 19 | }, 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/button/model.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type State = { 4 | status: 'green' | 'red', 5 | }; 6 | 7 | export const initialState: State = { 8 | status: 'green', 9 | }; 10 | 11 | export type Patch = { 12 | type: 'Toggle', 13 | }; 14 | 15 | export function reduce(state: State, patch: Patch): State { 16 | switch (patch.type) { 17 | case 'Toggle': 18 | return { 19 | ...state, 20 | status: state.status === 'green' ? 'red' : 'green', 21 | }; 22 | default: 23 | return state; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/button/view.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { PureComponent } from 'react'; 3 | import * as ButtonModel from './model'; 4 | 5 | type Props = { 6 | dispatch: (patch: ButtonModel.Patch) => void, 7 | state: ButtonModel.State, 8 | }; 9 | 10 | export default class Button extends PureComponent { 11 | handleClickButton = (): void => { 12 | this.props.dispatch({ 13 | type: 'Toggle', 14 | }); 15 | }; 16 | 17 | render() { 18 | return ( 19 |
20 | 28 |
29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/counter/__tests__/__snapshots__/model.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`test IncrementByOne 1`] = ` 2 | Object { 3 | "count": 1 4 | } 5 | `; 6 | 7 | exports[`test IncrementByTwo 1`] = ` 8 | Object { 9 | "count": 2 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/counter/__tests__/__snapshots__/view.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`test default 1`] = ` 2 |
3 |

4 | Count: 5 | 0 6 |

7 |
8 | `; 9 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/counter/__tests__/model.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as Test from '../../test'; 3 | import * as CounterModel from '../model'; 4 | 5 | Test.snapshotReduce(CounterModel.reduce, { 6 | IncrementByOne: { 7 | patch: {type: 'IncrementByOne'}, 8 | state: CounterModel.initialState, 9 | }, 10 | IncrementByTwo: { 11 | patch: {type: 'IncrementByTwo'}, 12 | state: CounterModel.initialState, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/counter/__tests__/view.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import * as Test from '../../test'; 4 | import * as CounterModel from '../model'; 5 | import Counter from '../view'; 6 | 7 | const defaultProps = { 8 | dispatch: Test.dispatch, 9 | state: CounterModel.initialState, 10 | }; 11 | 12 | Test.snapshotComponent(Counter, { 13 | 'default': defaultProps, 14 | }); 15 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/counter/model.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type State = { 4 | count: number, 5 | }; 6 | 7 | export const initialState: State = { 8 | count: 0, 9 | }; 10 | 11 | export type Patch = { 12 | type: 'IncrementByOne', 13 | } | { 14 | type: 'IncrementByTwo', 15 | }; 16 | 17 | export function reduce(state: State, patch: Patch): State { 18 | switch (patch.type) { 19 | case 'IncrementByOne': 20 | return { 21 | ...state, 22 | count: state.count + 1, 23 | }; 24 | case 'IncrementByTwo': 25 | return { 26 | ...state, 27 | count: state.count + 2, 28 | }; 29 | default: 30 | return state; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/counter/view.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { PureComponent } from 'react'; 3 | import * as CounterModel from './model'; 4 | 5 | type Props = { 6 | state: CounterModel.State, 7 | }; 8 | 9 | export default class Counter extends PureComponent { 10 | render() { 11 | return ( 12 |
13 |

Count: {this.props.state.count}

14 |
15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/effect.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type {Ship} from 'redux-ship'; 3 | import {call} from 'redux-ship'; 4 | 5 | export type Effect = { 6 | type: 'HttpRequest', 7 | url: string, 8 | }; 9 | 10 | export async function run(effect: Effect): Promise { 11 | switch (effect.type) { 12 | case 'HttpRequest': { 13 | const response = await fetch(effect.url); 14 | return await response.text(); 15 | } 16 | default: 17 | return; 18 | } 19 | } 20 | 21 | export function httpRequest(url: string): Ship { 22 | return call({ 23 | type: 'HttpRequest', 24 | url, 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import 'babel-polyfill'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import * as Ship from 'redux-ship'; 6 | import {logControl} from 'redux-ship-logger'; 7 | import Index from './view'; 8 | import './index.css'; 9 | import store from './store'; 10 | import * as Controller from './controller'; 11 | import * as Effect from './effect'; 12 | 13 | function dispatch(action: Controller.Action): void { 14 | Ship.run(Effect.run, store.dispatch, store.getState, logControl(Controller.control)(action)); 15 | } 16 | 17 | function render() { 18 | ReactDOM.render( 19 | , 23 | document.getElementById('root') 24 | ); 25 | } 26 | 27 | store.subscribe(render); 28 | render(); 29 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/random-gif-pair-pair/__tests__/model.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as Test from '../../test'; 3 | import * as RandomGifPairPairModel from '../model'; 4 | 5 | Test.snapshotReduce(RandomGifPairPairModel.reduce, { 6 | firstSecond: { 7 | patch: { 8 | first: {second: {type: 'LoadStart'}}, 9 | }, 10 | state: RandomGifPairPairModel.initialState, 11 | }, 12 | secondFirst: { 13 | patch: { 14 | second: {first: {type: 'LoadStart'}}, 15 | }, 16 | state: RandomGifPairPairModel.initialState, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/random-gif-pair-pair/__tests__/view.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import * as Test from '../../test'; 4 | import * as RandomGifPairPairModel from '../model'; 5 | import RandomGifPairPair from '../view'; 6 | 7 | const defaultProps = { 8 | dispatch: Test.dispatch, 9 | state: RandomGifPairPairModel.initialState, 10 | tagsPair: { 11 | first: { 12 | first: 'cats', 13 | second: 'dogs', 14 | }, 15 | second: { 16 | first: 'lemurs', 17 | second: 'minions', 18 | }, 19 | }, 20 | }; 21 | 22 | Test.snapshotComponent(RandomGifPairPair, { 23 | 'default': defaultProps, 24 | }); 25 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/random-gif-pair-pair/model.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as RandomGifPairModel from '../random-gif-pair/model'; 3 | 4 | export type State = { 5 | first: RandomGifPairModel.State, 6 | second: RandomGifPairModel.State, 7 | }; 8 | 9 | export const initialState: State = { 10 | first: RandomGifPairModel.initialState, 11 | second: RandomGifPairModel.initialState, 12 | }; 13 | 14 | export type Patch = { 15 | first?: RandomGifPairModel.Patch, 16 | second?: RandomGifPairModel.Patch, 17 | }; 18 | 19 | export function reduce(state: State, patch: Patch): State { 20 | return { 21 | ...state, 22 | ...patch.first && {first: RandomGifPairModel.reduce(state.first, patch.first)}, 23 | ...patch.second && {second: RandomGifPairModel.reduce(state.second, patch.second)}, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/random-gif-pair/__tests__/__snapshots__/model.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`test first 1`] = ` 2 | Object { 3 | "first": Object { 4 | "gifUrl": null, 5 | "isLoading": true 6 | }, 7 | "second": Object { 8 | "gifUrl": null, 9 | "isLoading": false 10 | } 11 | } 12 | `; 13 | 14 | exports[`test second 1`] = ` 15 | Object { 16 | "first": Object { 17 | "gifUrl": null, 18 | "isLoading": false 19 | }, 20 | "second": Object { 21 | "gifUrl": null, 22 | "isLoading": true 23 | } 24 | } 25 | `; 26 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/random-gif-pair/__tests__/__snapshots__/view.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`test default 1`] = ` 2 |
4 |
6 | waiting logo 10 | 15 |
16 |
18 | waiting logo 22 | 27 |
28 |
29 | `; 30 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/random-gif-pair/__tests__/model.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as Test from '../../test'; 3 | import * as RandomGifPairModel from '../model'; 4 | 5 | Test.snapshotReduce(RandomGifPairModel.reduce, { 6 | first: { 7 | patch: { 8 | first: {type: 'LoadStart'}, 9 | }, 10 | state: RandomGifPairModel.initialState, 11 | }, 12 | second: { 13 | patch: { 14 | second: {type: 'LoadStart'}, 15 | }, 16 | state: RandomGifPairModel.initialState, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/random-gif-pair/__tests__/view.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import * as Test from '../../test'; 4 | import * as RandomGifPairModel from '../model'; 5 | import RandomGifPair from '../view'; 6 | 7 | const defaultProps = { 8 | dispatch: Test.dispatch, 9 | state: RandomGifPairModel.initialState, 10 | tags: { 11 | first: 'cats', 12 | second: 'dogs', 13 | }, 14 | }; 15 | 16 | Test.snapshotComponent(RandomGifPair, { 17 | 'default': defaultProps, 18 | }); 19 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/random-gif-pair/model.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as RandomGifModel from '../random-gif/model'; 3 | 4 | export type State = { 5 | first: RandomGifModel.State, 6 | second: RandomGifModel.State, 7 | }; 8 | 9 | export const initialState: State = { 10 | first: RandomGifModel.initialState, 11 | second: RandomGifModel.initialState, 12 | }; 13 | 14 | export type Patch = { 15 | first?: RandomGifModel.Patch, 16 | second?: RandomGifModel.Patch, 17 | }; 18 | 19 | export function reduce(state: State, patch: Patch): State { 20 | return { 21 | ...state, 22 | ...patch.first && {first: RandomGifModel.reduce(state.first, patch.first)}, 23 | ...patch.second && {second: RandomGifModel.reduce(state.second, patch.second)}, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/random-gif-pair/view.css: -------------------------------------------------------------------------------- 1 | .RandomGifPair { 2 | display: flex; 3 | flex-direction: row; 4 | flex-wrap: wrap; 5 | justify-content: center; 6 | } 7 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/random-gif/__tests__/__snapshots__/model.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`test LoadStart 1`] = ` 2 | Object { 3 | "gifUrl": null, 4 | "isLoading": true 5 | } 6 | `; 7 | 8 | exports[`test LoadSuccess 1`] = ` 9 | Object { 10 | "gifUrl": "https://media2.giphy.com/media/m7ychnf9zOVm8/giphy.gif", 11 | "isLoading": false 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/random-gif/__tests__/model.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as Test from '../../test'; 3 | import * as RandomGifModel from '../model'; 4 | 5 | Test.snapshotReduce(RandomGifModel.reduce, { 6 | LoadStart: { 7 | patch: {type: 'LoadStart'}, 8 | state: RandomGifModel.initialState, 9 | }, 10 | LoadSuccess: { 11 | patch: { 12 | type: 'LoadSuccess', 13 | gifUrl: 'https://media2.giphy.com/media/m7ychnf9zOVm8/giphy.gif', 14 | }, 15 | state: RandomGifModel.initialState, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/random-gif/__tests__/view.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import * as Test from '../../test'; 4 | import * as RandomGifModel from '../model'; 5 | import RandomGif from '../view'; 6 | 7 | const defaultProps = { 8 | dispatch: Test.dispatch, 9 | state: RandomGifModel.initialState, 10 | tag: 'cats', 11 | }; 12 | 13 | Test.snapshotComponent(RandomGif, { 14 | 'default': defaultProps, 15 | loaded: { 16 | ...defaultProps, 17 | state: { 18 | ...defaultProps.state, 19 | gifUrl: 'https://media2.giphy.com/media/m7ychnf9zOVm8/giphy.gif', 20 | isLoading: false, 21 | }, 22 | }, 23 | loading: { 24 | ...defaultProps, 25 | state: { 26 | ...defaultProps.state, 27 | isLoading: true, 28 | }, 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/random-gif/model.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type State = { 4 | gifUrl: ?string, 5 | isLoading: bool, 6 | }; 7 | 8 | export const initialState: State = { 9 | gifUrl: null, 10 | isLoading: false, 11 | }; 12 | 13 | export type Patch = { 14 | type: 'LoadStart', 15 | } | { 16 | type: 'LoadSuccess', 17 | gifUrl: string, 18 | }; 19 | 20 | export function reduce(state: State, patch: Patch): State { 21 | switch (patch.type) { 22 | case 'LoadStart': 23 | return { 24 | ...state, 25 | isLoading: true, 26 | }; 27 | case 'LoadSuccess': 28 | return { 29 | ...state, 30 | isLoading: false, 31 | gifUrl: patch.gifUrl, 32 | }; 33 | default: 34 | return state; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/random-gif/view.css: -------------------------------------------------------------------------------- 1 | .RandomGif { 2 | display: flex; 3 | flex-direction: column; 4 | margin: 20px; 5 | } 6 | 7 | .RandomGif-picture { 8 | height: 300px; 9 | width: 300px; 10 | } 11 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/store.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import {applyMiddleware, createStore} from 'redux'; 3 | import {logCommit} from 'redux-ship-logger'; 4 | import * as Controller from './controller'; 5 | import * as Model from './model'; 6 | 7 | const middlewares = [ 8 | logCommit(Controller.applyCommit), 9 | ]; 10 | 11 | function reduce(state, commit) { 12 | return Model.reduce(state, Controller.applyCommit(state, commit)); 13 | } 14 | 15 | export default createStore(reduce, Model.initialState, applyMiddleware(...middlewares)); 16 | -------------------------------------------------------------------------------- /redux-ship-clarus/src/view.css: -------------------------------------------------------------------------------- 1 | .Index { 2 | text-align: center; 3 | } 4 | 5 | .Index-logo { 6 | animation: Index-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | @keyframes Index-logo-spin { 11 | from { transform: rotate(0deg); } 12 | to { transform: rotate(360deg); } 13 | } 14 | 15 | .Index-header { 16 | background-color: #222; 17 | height: 150px; 18 | padding: 20px; 19 | color: white; 20 | } 21 | 22 | .Index-intro { 23 | margin: 30px; 24 | } 25 | 26 | .Index-randomGif { 27 | display: flex; 28 | justify-content: center; 29 | } 30 | 31 | .Index-footer { 32 | margin-bottom: 30px; 33 | margin-top: 80px; 34 | } 35 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/README.md: -------------------------------------------------------------------------------- 1 | # Redux Subspace Example 2 | 3 | Example using [redux-subspace](https://github.com/ioof-holdings/redux-subspace/), a library for creating isolated sub-apps with a global Redux store. It also has support for [React](https://facebook.github.io/react/), which was used to create this example. 4 | 5 | To run the locally: 6 | 7 | ```sh 8 | git clone https://github.com/slorber/scalable-frontend-with-elm-or-redux.git 9 | 10 | cd scalable-frontend-with-elm-or-redux/redux-subspace-mpeyper 11 | npm install 12 | npm start 13 | ``` 14 | 15 | Or check out the [sandbox](https://codesandbox.io/s/github/slorber/scalable-frontend-with-elm-or-redux/tree/master/redux-subspace-mpeyper). 16 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todos", 3 | "version": "1.0.0", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts": "^1.0.2" 7 | }, 8 | "dependencies": { 9 | "react": "^15.6.1", 10 | "react-dom": "^15.6.1", 11 | "react-redux": "^5.0.6", 12 | "react-redux-subspace": "^2.0.5-alpha", 13 | "redux": "^3.7.2", 14 | "redux-devtools-extension": "^2.13.2", 15 | "redux-subspace": "^2.0.5-alpha", 16 | "redux-subspace-wormhole": "^2.0.5-alpha", 17 | "redux-thunk": "^2.2.0" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "eject": "react-scripts eject" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux Subspace Example 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/api.js: -------------------------------------------------------------------------------- 1 | export default class API { 2 | getRandomGifUrl() { 3 | return fetch(`https://api.giphy.com/v1/gifs/random?api_key=26ae311d361143eda202a3670a1b0c63`) 4 | .then((response) => response.json()) 5 | .then((json => json.data.fixed_width_small_url)) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/app/index.js: -------------------------------------------------------------------------------- 1 | import App from './App' 2 | import reducer from './reducer' 3 | 4 | export { 5 | App, 6 | reducer 7 | } 8 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/app/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { namespaced } from 'redux-subspace' 3 | import { reducer as randomGifReducer } from '../randomGif' 4 | import { reducer as randomGifPairReducer } from '../randomGifPair' 5 | import { reducer as randomGifPairPairReducer } from '../randomGifPairPair' 6 | import { reducer as buttonReducer } from '../button' 7 | import { reducer as counterReducer } from '../counter' 8 | 9 | export default combineReducers({ 10 | randomGif: namespaced('randomGif')(randomGifReducer), 11 | randomGifPair: namespaced('randomGifPair')(randomGifPairReducer), 12 | randomGifPairPair: namespaced('randomGifPairPair')(randomGifPairPairReducer), 13 | button: namespaced('button')(buttonReducer), 14 | counter: namespaced('counter')(counterReducer) 15 | }) 16 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/button/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { toggle } from './actions' 4 | 5 | const Button = ({ toggled, toggle }) => { 6 | const style = { backgroundColor: toggled ? "green" : "red", color: "white"} 7 | 8 | return ( 9 | 10 | ) 11 | } 12 | 13 | const mapStateToProps = (state) => ({ 14 | toggled: state.toggled 15 | }) 16 | 17 | const actionCreators = { toggle } 18 | 19 | export default connect(mapStateToProps, actionCreators)(Button) 20 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/button/actions.js: -------------------------------------------------------------------------------- 1 | export const TOGGLE = 'TOGGLE' 2 | 3 | export const toggle = () => ({ type: TOGGLE }) 4 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/button/index.js: -------------------------------------------------------------------------------- 1 | import Button from './Button' 2 | import reducer from './reducer' 3 | import { TOGGLE } from './actions' 4 | 5 | export { 6 | Button, 7 | reducer, 8 | TOGGLE 9 | } 10 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/button/reducer.js: -------------------------------------------------------------------------------- 1 | import { TOGGLE } from './actions' 2 | 3 | const initialState = { toggled: false } 4 | 5 | export default (state = initialState, action) => { 6 | switch (action.type) { 7 | case TOGGLE: 8 | return { ...state, toggled: !state.toggled } 9 | default: 10 | return state 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/counter/Counter.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | const Counter = ({ count }) => ( 5 |

Counter: {count}

6 | ) 7 | 8 | const mapStateToProps = (state) => ({ 9 | count: state.count 10 | }) 11 | 12 | export default connect(mapStateToProps)(Counter) 13 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/counter/actions.js: -------------------------------------------------------------------------------- 1 | export const INCREMENT = 'INCREMENT' 2 | 3 | export const increment = () => ({ type: INCREMENT }) 4 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/counter/index.js: -------------------------------------------------------------------------------- 1 | import Counter from './Counter' 2 | import reducer from './reducer' 3 | import { increment } from './actions' 4 | 5 | export { 6 | Counter, 7 | reducer, 8 | increment 9 | } 10 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/counter/reducer.js: -------------------------------------------------------------------------------- 1 | import { TOGGLE } from '../button' 2 | import { INCREMENT } from './actions' 3 | 4 | const initialState = { 5 | useMultiplier: false, 6 | count: 0 7 | } 8 | 9 | export default (state = initialState, action) => { 10 | switch (action.type) { 11 | case TOGGLE: 12 | return { ...state, useMultiplier: !state.useMultiplier } 13 | case INCREMENT: { 14 | const increment = state.count >= 10 && state.useMultiplier ? 2 : 1 15 | return { ...state, count: state.count + increment } 16 | } 17 | default: 18 | return state 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | import store from './store' 5 | import { App } from './app' 6 | 7 | render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ) 13 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/randomGif/RandomGif.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { getGif } from './actions' 4 | 5 | const RandomGif = ({ loading, src, getGif }) => ( 6 |
7 | 8 | { src &&
Gif
} 9 |
10 | ) 11 | 12 | const mapStateToProps = (state) => ({ 13 | loading: state.loading, 14 | src: state.src 15 | }) 16 | 17 | const actionCreators = { getGif } 18 | 19 | export default connect(mapStateToProps, actionCreators)(RandomGif) 20 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/randomGif/actions.js: -------------------------------------------------------------------------------- 1 | import { increment } from '../counter' 2 | 3 | export const LOADING = 'LOADING' 4 | export const SET_SRC = 'SET_SRC' 5 | 6 | const loading = () => ({ type: LOADING }) 7 | 8 | const setGifSrc = (src) => ({ type: SET_SRC, src }) 9 | 10 | export const getGif = () => (dispatch, getState, api) => { 11 | dispatch(loading()) 12 | 13 | api.getRandomGifUrl() 14 | .then((url) => dispatch(setGifSrc(url))) 15 | .then(() => dispatch(increment())) 16 | } 17 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/randomGif/index.js: -------------------------------------------------------------------------------- 1 | import RandomGif from './RandomGif' 2 | import reducer from './reducer' 3 | 4 | export { 5 | RandomGif, 6 | reducer 7 | } 8 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/randomGif/reducer.js: -------------------------------------------------------------------------------- 1 | import { LOADING, SET_SRC } from './actions' 2 | 3 | const initialState = { loading: false } 4 | 5 | export default (state = initialState, action) => { 6 | switch (action.type) { 7 | case LOADING: 8 | return { ...state, loading: true} 9 | case SET_SRC: 10 | return { ...state, src: action.src, loading: false} 11 | default: 12 | return state 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/randomGifPair/RandomGifPair.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SubspaceProvider } from 'react-redux-subspace' 3 | import { RandomGif } from '../randomGif' 4 | 5 | const RandomGifPair = () => ( 6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | ) 19 | 20 | export default RandomGifPair 21 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/randomGifPair/index.js: -------------------------------------------------------------------------------- 1 | import RandomGifPair from './RandomGifPair' 2 | import reducer from './reducer' 3 | 4 | export { 5 | RandomGifPair, 6 | reducer 7 | } 8 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/randomGifPair/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { namespaced } from 'redux-subspace' 3 | import { reducer as randomGifReducer } from '../randomGif' 4 | 5 | export default combineReducers({ 6 | randomGif1: namespaced('randomGif1')(randomGifReducer), 7 | randomGif2: namespaced('randomGif2')(randomGifReducer) 8 | }) 9 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/randomGifPairPair/RandomGifPairPair.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SubspaceProvider } from 'react-redux-subspace' 3 | import { RandomGifPair } from '../randomGifPair' 4 | 5 | const RandomGifPairPair = () => ( 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | ) 15 | 16 | export default RandomGifPairPair 17 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/randomGifPairPair/index.js: -------------------------------------------------------------------------------- 1 | import RandomGifPairPair from './RandomGifPairPair' 2 | import reducer from './reducer' 3 | 4 | export { 5 | RandomGifPairPair, 6 | reducer 7 | } 8 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/randomGifPairPair/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { namespaced } from 'redux-subspace' 3 | import { reducer as randomGifPairReducer } from '../randomGifPair' 4 | 5 | export default combineReducers({ 6 | randomGifPair1: namespaced('randomGifPair1')(randomGifPairReducer), 7 | randomGifPair2: namespaced('randomGifPair2')(randomGifPairReducer) 8 | }) 9 | -------------------------------------------------------------------------------- /redux-subspace-mpeyper/src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux' 2 | import { composeWithDevTools } from 'redux-devtools-extension' 3 | import { applyMiddleware, globalActions } from 'redux-subspace' 4 | import thunk from 'redux-thunk' 5 | import wormhole from 'redux-subspace-wormhole' 6 | import API from './api' 7 | import { reducer } from './app' 8 | 9 | const middleware = applyMiddleware( 10 | thunk.withExtraArgument(new API()), 11 | globalActions('TOGGLE', 'INCREMENT'), 12 | wormhole('config') 13 | ) 14 | 15 | export default createStore(reducer, composeWithDevTools(middleware)) 16 | -------------------------------------------------------------------------------- /tom-binary-tree/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/bundle.js -------------------------------------------------------------------------------- /tom-binary-tree/README.md: -------------------------------------------------------------------------------- 1 | # Features 2 | 3 | - Basic components (`Button`, `Counter`, `RandomGif`) are decoupled. They are not augmented or expose a specific interface in order to solve this challenge 4 | - Compound components (`RandomGifPair`, `RandomGifPairOfPair`) are assembled using a general and reutilisable `compose` function 5 | - The only allowed way to comunicate with `Counter` is via its event `Increment(step)` 6 | - Business logic is put entirely into `Main` 7 | - `Main`'s rendering tree is a binary tree (via the `compose` function) providing a predicatable way to retrieve the state and send events to child components 8 | 9 | # Setup 10 | 11 | ```sh 12 | cd tom-binary-tree 13 | npm install 14 | npm run build 15 | open dist/index.html 16 | ``` -------------------------------------------------------------------------------- /tom-binary-tree/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | tom - Scalable frontend, with Elm or Redux 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tom-binary-tree/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tom-binary-tree", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "build": "webpack" 8 | }, 9 | "author": "Giulio Canti ", 10 | "license": "MIT", 11 | "dependencies": { 12 | "axios": "0.9.1", 13 | "react": "0.14.7", 14 | "react-dom": "0.14.7", 15 | "tom": "0.4.0" 16 | }, 17 | "devDependencies": { 18 | "babel": "5.8.34", 19 | "babel-core": "5.8.34", 20 | "babel-loader": "5.3.2", 21 | "webpack": "1.12.14" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tom-binary-tree/src/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // EVENTS 4 | 5 | class Toggle { 6 | update(model) { 7 | return { model: !model } 8 | } 9 | } 10 | 11 | // APP 12 | 13 | export default { 14 | 15 | init() { 16 | return { model: true } 17 | }, 18 | 19 | update(model, event) { 20 | return event.update(model) 21 | }, 22 | 23 | view(model, dispatch) { 24 | const onClick = () => dispatch(new Toggle()) 25 | const style = { backgroundColor: model ? 'green' : 'red' } 26 | return 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /tom-binary-tree/src/Counter.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // EVENTS 4 | 5 | export class Increment { 6 | constructor(step) { 7 | this.step = step 8 | } 9 | update(model) { 10 | return { model: model + this.step } 11 | } 12 | } 13 | 14 | // APP 15 | 16 | export default { 17 | 18 | init() { 19 | return { model: 0 } 20 | }, 21 | 22 | update(model, event) { 23 | return event.update(model) 24 | }, 25 | 26 | view(model) { 27 | return

Count: {model}

28 | } 29 | 30 | } -------------------------------------------------------------------------------- /tom-binary-tree/src/RandomGifPair.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import compose from './compose' 3 | import RandomGif from './RandomGif' 4 | 5 | const style = { 6 | display: 'flex', 7 | padding: '5px', 8 | border: '1px solid #ccc' 9 | } 10 | 11 | const template = (x, y) =>
{x}{y}
12 | 13 | export default class RandomGifPair { 14 | 15 | constructor(leftTopic, rightTopic) { 16 | const composition = compose( 17 | new RandomGif(leftTopic), 18 | new RandomGif(rightTopic), 19 | template 20 | ) 21 | this.init = composition.init 22 | this.update = composition.update 23 | this.view = composition.view 24 | this.run = composition.run 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /tom-binary-tree/src/RandomGifPairOfPair.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import compose from './compose' 3 | import RandomGifPair from './RandomGifPair' 4 | 5 | const style = { 6 | padding: '5px', 7 | border: '2px solid #555' 8 | } 9 | 10 | const template = (x, y) =>
{x}{y}
11 | 12 | export default class RandomGifPairOfPair { 13 | 14 | constructor(topLeftTopic, topRightTopic, bottomLeftTopic, bottomRightTopic) { 15 | const composition = compose( 16 | new RandomGifPair(topLeftTopic, topRightTopic), 17 | new RandomGifPair(bottomLeftTopic, bottomRightTopic), 18 | template 19 | ) 20 | this.init = composition.init 21 | this.update = composition.update 22 | this.view = composition.view 23 | this.run = composition.run 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /tom-binary-tree/src/index.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom' 2 | import { start } from 'tom' 3 | import Main from './Main' 4 | // import compose from './compose' 5 | 6 | const app = start(Main) 7 | // const app = start(compose(Main, Main)) // <= try this, it's fun 8 | app.view$.subscribe(view => ReactDOM.render(view, document.getElementById('app'))) 9 | -------------------------------------------------------------------------------- /tom-binary-tree/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | 3 | module.exports = { 4 | entry: './src/index', 5 | output: { 6 | path: './dist', 7 | filename: 'bundle.js' 8 | }, 9 | module: { 10 | loaders: [ 11 | { 12 | loader: 'babel', 13 | exclude: [/node_modules/] 14 | } 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /upward-message/Button.elm: -------------------------------------------------------------------------------- 1 | module Button where 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (style) 5 | import Html.Events exposing (onClick) 6 | 7 | type Action = Click 8 | 9 | type alias Model = Bool 10 | 11 | init : Model 12 | init = True 13 | 14 | update : Action -> Model -> Model 15 | update action model = not model 16 | 17 | view : Signal.Address Action -> Model -> Html 18 | view address model = 19 | button 20 | [ style [("backgroundColor", if model then "green" else "red")] 21 | , onClick address Click 22 | ] 23 | [ text "Click"] 24 | 25 | 26 | 27 | isActive : Model -> Bool 28 | isActive model = model -------------------------------------------------------------------------------- /upward-message/Counter.elm: -------------------------------------------------------------------------------- 1 | module Counter where 2 | 3 | import Html exposing (..) 4 | 5 | type Action = Increment Bool 6 | 7 | type alias Model = Int 8 | 9 | init : Model 10 | init = 0 11 | 12 | update : Action -> Model -> Model 13 | update action model = 14 | case action of 15 | Increment True -> 16 | if model >= 10 then model + 2 else model + 1 17 | Increment False -> 18 | model + 1 19 | 20 | increment : Bool -> Model -> Model 21 | increment button model = 22 | update (Increment button) model 23 | 24 | 25 | view : Signal.Address Action -> Model -> Html 26 | view address model = 27 | div [] 28 | [ text <| toString model] 29 | -------------------------------------------------------------------------------- /upward-message/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "." 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "3.0.0 <= v < 4.0.0", 12 | "evancz/elm-effects": "2.0.1 <= v < 3.0.0", 13 | "evancz/elm-html": "4.0.2 <= v < 5.0.0", 14 | "evancz/elm-http": "3.0.0 <= v < 4.0.0", 15 | "evancz/start-app": "2.0.2 <= v < 3.0.0" 16 | }, 17 | "elm-version": "0.16.0 <= v < 0.17.0" 18 | } --------------------------------------------------------------------------------