Skip to content

Commit

Permalink
Full experiment.
Browse files Browse the repository at this point in the history
  • Loading branch information
Jake Gibbons committed Feb 27, 2017
1 parent 5f12aca commit ba605d9
Show file tree
Hide file tree
Showing 14 changed files with 434 additions and 11 deletions.
14 changes: 12 additions & 2 deletions .eslintrc
Expand Up @@ -22,6 +22,16 @@
"key-spacing" : 0,
"jsx-quotes" : [2, "prefer-single"],
"max-len" : [2, 120, 2],
"object-curly-spacing" : [2, "always"]
}
"object-curly-spacing" : [2, "always"],
"comma-dangle": [
"error",
{
"arrays": "only-multiline",
"objects": "only-multiline",
"imports": "only-multiline",
"exports": "only-multiline",
"functions": "ignore",
},
],
},
}
4 changes: 2 additions & 2 deletions config/project.config.js
Expand Up @@ -24,8 +24,8 @@ const config = {
// ----------------------------------
// Server Configuration
// ----------------------------------
server_host : ip.address(), // use string 'localhost' to prevent exposure on local network
server_port : process.env.PORT || 3000,
server_host : 'localhost', // use string 'localhost' to prevent exposure on local network
server_port : process.env.PORT || 3001,

// ----------------------------------
// Compiler Configuration
Expand Down
8 changes: 6 additions & 2 deletions package.json
Expand Up @@ -19,7 +19,8 @@
"deploy": "better-npm-run deploy",
"deploy:dev": "better-npm-run deploy:dev",
"deploy:prod": "better-npm-run deploy:prod",
"codecov": "cat coverage/*/lcov.info | codecov"
"codecov": "cat coverage/*/lcov.info | codecov",
"syncProd": "better-npm-run syncProd"
},
"betterScripts": {
"compile": {
Expand Down Expand Up @@ -49,7 +50,7 @@
}
},
"deploy:prod": {
"command": "npm run deploy",
"command": "npm run deploy && npm run syncProd",
"env": {
"NODE_ENV": "production",
"DEBUG": "app:*"
Expand All @@ -61,6 +62,9 @@
"DEBUG": "app:*"
}
},
"syncProd": {
"command": "aws s3 sync ./dist/ s3://jayseeg.com/"
},
"test": {
"command": "node ./node_modules/karma/bin/karma start config/karma.config",
"env": {
Expand Down
4 changes: 4 additions & 0 deletions src/components/Header/Header.js
Expand Up @@ -12,6 +12,10 @@ export const Header = () => (
<Link to='/counter' activeClassName='route--active'>
Counter
</Link>
{' · '}
<Link to='/conter' activeClassName='route--active'>
Conter
</Link>
</div>
)

Expand Down
2 changes: 1 addition & 1 deletion src/main.js
Expand Up @@ -16,7 +16,7 @@ const MOUNT_NODE = document.getElementById('root')

let render = () => {
const routes = require('./routes/index').default(store)

console.log({ routes })
ReactDOM.render(
<AppContainer store={store} routes={routes} />,
MOUNT_NODE
Expand Down
32 changes: 32 additions & 0 deletions src/routes/Conter/components/Conter.js
@@ -0,0 +1,32 @@
import React from 'react'

export const Conter = (props) => (
<div style={{ margin: '0 auto' }} >
<h2>Conter: {props.conter}</h2>
<button className='btn btn-default' onClick={props.reset}>
Reset
</button>
{' '}
<button className='btn btn-default' onClick={props.increment}>
Increment
</button>
{' '}
<button className='btn btn-default' onClick={props.doubleAsync}>
Double (Async)
</button>
{' '}
<button className='btn btn-default' onClick={props.exponentAsync}>
Exponent (Async)
</button>
</div>
)

Conter.propTypes = {
conter: React.PropTypes.number.isRequired,
exponentAsync: React.PropTypes.func.isRequired,
doubleAsync: React.PropTypes.func.isRequired,
reset: React.PropTypes.func.isRequired,
increment: React.PropTypes.func.isRequired,
}

export default Conter
45 changes: 45 additions & 0 deletions src/routes/Conter/containers/ConterContainer.js
@@ -0,0 +1,45 @@
import { connect } from 'react-redux'
import {
increment,
reset,
doubleAsync,
exponentAsync,
} from '../modules/conter'

/* This is a container component. Notice it does not contain any JSX,
nor does it import React. This component is **only** responsible for
wiring in the actions and state necessary to render a presentational
component - in this case, the conter: */

import Conter from '../components/Conter'

/* Object of action creators (can also be function that returns object).
Keys will be passed as props to presentational components. Here we are
implementing our wrapper around increment; the component doesn't care */

const mapDispatchToProps = {
increment : () => increment(1),
reset,
doubleAsync,
exponentAsync,
}

const mapStateToProps = (state) => ({
conter : state.conter
})

/* Note: mapStateToProps is where you should use `reselect` to create selectors, ie:
import { createSelector } from 'reselect'
const conter = (state) => state.conter
const tripleCount = createSelector(conter, (count) => count * 3)
const mapStateToProps = (state) => ({
conter: tripleCount(state)
})
Selectors can compute derived data, allowing Redux to store the minimal possible state.
Selectors are efficient. A selector is not recomputed unless one of its arguments change.
Selectors are composable. They can be used as input to other selectors.
https://github.com/reactjs/reselect */

export default connect(mapStateToProps, mapDispatchToProps)(Conter)
24 changes: 24 additions & 0 deletions src/routes/Conter/index.js
@@ -0,0 +1,24 @@
import { injectReducer } from '../../store/reducers'

export default (store) => ({
path : 'conter',
/* Async getComponent is only invoked when route matches */
getComponent (nextState, cb) {
/* Webpack - use 'require.ensure' to create a split point
and embed an async module loader (jsonp) when bundling */
require.ensure([], (require) => {
/* Webpack - use require callback to define
dependencies for bundling */
const Conter = require('./containers/ConterContainer').default
const reducer = require('./modules/conter').default

/* Add the reducer to the store on key 'conter' */
injectReducer(store, { key: 'conter', reducer })

/* Return getComponent */
cb(null, Conter)

/* Webpack named bundle */
}, 'conter')
}
})
80 changes: 80 additions & 0 deletions src/routes/Conter/modules/conter.js
@@ -0,0 +1,80 @@
// ------------------------------------
// Constants
// ------------------------------------
export const CONTER_INCREMENT = 'CONTER_INCREMENT'
export const CONTER_RESET = 'CONTER_RESET'
export const CONTER_DOUBLE_ASYNC = 'CONTER_DOUBLE_ASYNC'
export const CONTER_EXPONENT_ASYNC = 'CONTER_EXPONENT_ASYNC'

// ------------------------------------
// Actions
// ------------------------------------
export function increment (value = 1) {
return {
type : CONTER_INCREMENT,
payload : value
}
}

export function reset () {
return { type: CONTER_RESET }
}

/* This is a thunk, meaning it is a function that immediately
returns a function for lazy evaluation. It is incredibly useful for
creating async actions, especially when combined with redux-thunk! */

export const doubleAsync = () => {
return (dispatch, getState) => {
return new Promise((resolve) => {
setTimeout(() => {
dispatch({
type : CONTER_DOUBLE_ASYNC,
payload : getState().conter
})
resolve()
}, 333)
})
}
}

export const exponentAsync = () => {
return (dispatch, getState) => {
return new Promise((resolve) => {
setTimeout(() => {
dispatch({
type : CONTER_EXPONENT_ASYNC,
payload : getState().conter
})
resolve()
}, 666)
})
}
}

export const actions = {
increment,
reset,
doubleAsync,
exponentAsync,
}

// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[CONTER_INCREMENT]: (state, action) => state + action.payload,
[CONTER_RESET]: () => 0,
[CONTER_DOUBLE_ASYNC]: (state, action) => state * 2,
[CONTER_EXPONENT_ASYNC]: (state, action) => state * state,
}

// ------------------------------------
// Reducer
// ------------------------------------
const initialState = 0
export default function conterReducer (state = initialState, action) {
const handler = ACTION_HANDLERS[action.type]

return handler ? handler(state, action) : state
}
4 changes: 2 additions & 2 deletions src/routes/Home/components/HomeView.js
Expand Up @@ -4,9 +4,9 @@ import './HomeView.scss'

export const HomeView = () => (
<div>
<h4>Welcome!</h4>
<h4>Welcome home mother fucker!</h4>
<img
alt='This is a duck, because Redux!'
alt='This is a duck mother fucker, because Redux!'
className='duck'
src={DuckImage} />
</div>
Expand Down
6 changes: 4 additions & 2 deletions src/routes/index.js
Expand Up @@ -2,6 +2,7 @@
import CoreLayout from '../layouts/CoreLayout'
import Home from './Home'
import CounterRoute from './Counter'
import ConterRoute from './Conter'

/* Note: Instead of using JSX, we recommend using react-router
PlainRoute objects to build route definitions. */
Expand All @@ -11,8 +12,9 @@ export const createRoutes = (store) => ({
component : CoreLayout,
indexRoute : Home,
childRoutes : [
CounterRoute(store)
]
CounterRoute(store),
ConterRoute(store),
],
})

/* Note: childRoutes can be chunked or otherwise loaded programmatically
Expand Down
80 changes: 80 additions & 0 deletions tests/routes/Conter/components/Conter.spec.js
@@ -0,0 +1,80 @@
import React from 'react'
import { bindActionCreators } from 'redux'
import { Conter } from 'routes/Conter/components/Conter'
import { shallow } from 'enzyme'

describe('(Component) Conter', () => {
let _props, _spies, _wrapper

beforeEach(() => {
_spies = {}
_props = {
conter : 5,
...bindActionCreators({
doubleAsync : (_spies.doubleAsync = sinon.spy()),
increment : (_spies.increment = sinon.spy())
}, _spies.dispatch = sinon.spy())
}
_wrapper = shallow(<Conter {..._props} />)
})

it('Should render as a <div>.', () => {
expect(_wrapper.is('div')).to.equal(true)
})

it('Should render with an <h2> that includes Sample Conter text.', () => {
expect(_wrapper.find('h2').text()).to.match(/Conter:/)
})

it('Should render props.conter at the end of the sample conter <h2>.', () => {
expect(_wrapper.find('h2').text()).to.match(/5$/)
_wrapper.setProps({ conter: 8 })
expect(_wrapper.find('h2').text()).to.match(/8$/)
})

it('Should render exactly two buttons.', () => {
expect(_wrapper.find('button')).to.have.length(2)
})

describe('An increment button...', () => {
let _button

beforeEach(() => {
_button = _wrapper.find('button').filterWhere(a => a.text() === 'Increment')
})

it('has bootstrap classes', () => {
expect(_button.hasClass('btn btn-default')).to.be.true
})

it('Should dispatch a `increment` action when clicked', () => {
_spies.dispatch.should.have.not.been.called

_button.simulate('click')

_spies.dispatch.should.have.been.called
_spies.increment.should.have.been.called
})
})

describe('A Double (Async) button...', () => {
let _button

beforeEach(() => {
_button = _wrapper.find('button').filterWhere(a => a.text() === 'Double (Async)')
})

it('has bootstrap classes', () => {
expect(_button.hasClass('btn btn-default')).to.be.true
})

it('Should dispatch a `doubleAsync` action when clicked', () => {
_spies.dispatch.should.have.not.been.called

_button.simulate('click')

_spies.dispatch.should.have.been.called
_spies.doubleAsync.should.have.been.called
})
})
})
17 changes: 17 additions & 0 deletions tests/routes/Conter/index.spec.js
@@ -0,0 +1,17 @@
import ConterRoute from 'routes/Conter'

describe('(Route) Conter', () => {
let _route

beforeEach(() => {
_route = ConterRoute({})
})

it('Should return a route configuration object', () => {
expect(typeof _route).to.equal('object')
})

it('Configuration should contain path `conter`', () => {
expect(_route.path).to.equal('conter')
})
})

0 comments on commit ba605d9

Please sign in to comment.