) => [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 |
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 |

10 |
15 |
16 |
18 |

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 &&
}
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 | }
--------------------------------------------------------------------------------