Created
June 23, 2018 19:48
-
-
Save ronen-e/efc469276d827a6121f0d2b6765102be to your computer and use it in GitHub Desktop.
Simple react-redux demo with translations for Browser
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
header { | |
margin-top: 20px; | |
display: flex; | |
justify-content: space-around; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<script src="https://unpkg.com/@reactivex/[email protected]/dist/global/Rx.js"></script> | |
<script src="https://cdn.jsdelivr.net/lodash/4/lodash.min.js"></script> | |
<script src="https://fb.me/react-15.1.0.js"></script> | |
<script src="https://fb.me/react-dom-15.1.0.js"></script> | |
<script src="https://code.jquery.com/jquery-3.0.0.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.0/redux.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/4.0.0/react-redux.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux-thunk/2.3.0/redux-thunk.js"></script> | |
<meta name="viewport" content="width=device-width"> | |
<title>REACT REDUX DEMO</title> | |
</head> | |
<body> | |
<div id="app"></div> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { Provider, connect } = ReactRedux; | |
const { render } = ReactDOM; | |
const { Component, PropTypes } = React; | |
const { createStore, combineReducers, applyMiddleware, bindActionCreators } = Redux; | |
const thunk = ReduxThunk.default; | |
// TRANSLATIONS | |
const translations = { | |
'en': { | |
'submissions': 'Last submissions', | |
'add': 'Add', | |
'remove': 'Remove', | |
'submit': 'Submit', | |
'loading': 'Loading...', | |
'counter': '{{ num }} / {{ total }}' | |
} | |
} | |
const translate = (translations, path) => _.get(translations, path, path); | |
// UTILS - SINGLETON | |
_.templateSettings.interpolate = /{{([\s\S]+?)}}/g; | |
const utils = { | |
locale: 'en', | |
translate(path, options = {}) { | |
const locale = _.get(translations, utils.locale); | |
let result = translate(locale, path); | |
if (options.params) { | |
const compiled = _.template(result); | |
result = compiled(options.params); | |
} | |
return result; | |
} | |
} | |
const t = utils.translate; | |
// TYPES | |
const types = { | |
ADD_ITEM: 'ADD_ITEM', | |
REMOVE_ITEM: 'REMOVE_ITEM', | |
SUBMIT: 'SUBMIT', | |
SUBMIT_START: 'SUBMIT_START', | |
SUBMIT_SUCCESS: 'SUBMIT_SUCCESS', | |
SUBMIT_FAIL: 'SUBMIT_FAIL', | |
} | |
// ACTIONS | |
const actions = { | |
addItem() { | |
return { | |
type: types.ADD_ITEM | |
} | |
}, | |
removeItem(payload) { | |
return { | |
type: types.REMOVE_ITEM, | |
payload | |
} | |
}, | |
submit(payload) { | |
return function(dispatch, getState) { | |
dispatch(actions.submitStart()) | |
return new Promise( (resolve) => _.delay(resolve, 1000)) | |
.then(() => dispatch({ type: types.SUBMIT, payload })) | |
.then(() => dispatch(actions.submitSuccess()) ) | |
} | |
}, | |
submitStart() { | |
return { type: types.SUBMIT_START } | |
}, | |
submitSuccess() { | |
return { type: types.SUBMIT_SUCCESS } | |
}, | |
submitFail() { | |
return { type: types.SUBMIT_FAIL } | |
} | |
} | |
// REDUCERS | |
const itemsReducer = function(state = [], action) { | |
switch(action.type) { | |
case types.ADD_ITEM: | |
return state.concat({id: _.uniqueId()}); | |
case types.REMOVE_ITEM: | |
return _.without(state, action.payload); | |
} | |
return state; | |
} | |
const configReducer = function(state = {maxItems: 10}, action) { | |
return state | |
} | |
const loaderReducer = function(state = false, action) { | |
switch(action.type) { | |
case types.SUBMIT_START: | |
return true; | |
case types.SUBMIT_SUCCESS: | |
case types.SUBMIT_FAIL: | |
return false; | |
} | |
return state; | |
} | |
const submissionsReducer = (state = [], action) => { | |
switch (action.type) { | |
case types.SUBMIT: | |
return Array.from(action.payload); | |
} | |
return state; | |
} | |
const rootReducer = combineReducers({ | |
emails: combineReducers({ | |
items: itemsReducer, | |
config: configReducer, | |
loading: loaderReducer, | |
submissions: submissionsReducer | |
}) | |
}) | |
// INITIAL STATE | |
const initialState = {}; | |
// STORE | |
const store = createStore(rootReducer, initialState, applyMiddleware(thunk)); | |
// COMPONENT - ListItem | |
class ListItem extends React.Component { | |
get value() { | |
return this.refs.input.value; | |
} | |
render() { | |
var { addItem, removeItem, index, maxItems } = this.props; | |
return ( | |
<li> | |
<input ref="input" type="text" /> | |
<button onClick={addItem}>+</button> | |
<button onClick={removeItem}>-</button> | |
<span>{t('counter', {params:{num: index+1, total: maxItems}})}</span> | |
</li> | |
) | |
} | |
} | |
// COMPONENT - List | |
class List extends Component { | |
get values() { | |
return _(this.refs) | |
.map(ref => ref.getWrappedInstance()) | |
.map(input => input.value) | |
.map(_.trim) | |
.filter(Boolean) | |
.value() | |
} | |
get itemsComponent() { | |
const { items, addItem, removeItem } = this.props; | |
return items.map( (item, i) => { | |
return ( | |
<ListItem key={item.id} ref={item.id} | |
index={i} | |
addItem={() => addItem(item)} | |
removeItem={() => removeItem(item)} /> | |
) | |
}) | |
} | |
render() { | |
return ( | |
<ul> | |
{this.itemsComponent} | |
</ul> | |
) | |
} | |
} | |
// COMPONENT - Header | |
let Header = (props, context) => { | |
// console.log('header props', props) | |
return ( | |
<header> | |
<button onClick={props.addItem}>{t('add')}</button> | |
<button onClick={props.removeItem}>{t('remove')}</button> | |
<button onClick={props.submit}>{t('submit')}</button> | |
</header> | |
) | |
} | |
// COMPONENT - Submissions | |
let Submissions = (props, context) => { | |
var items = props.submissions; | |
const Loader = props.loading ? (<p>{t('loading')}</p>) : null; | |
return ( | |
<article> | |
<header>Last submissions</header> | |
{Loader} | |
<ul> | |
{items.map( (val,i) => <li key={i}>{val}</li> )} | |
</ul> | |
</article> | |
); | |
} | |
// COMPONENT - App | |
class App extends React.Component { | |
constructor(props, context) { | |
super(props, context); | |
_.bindAll(this, ['addItem', 'removeLastItem', 'submit']); | |
} | |
get isAddItemEnabled() { | |
return this.props.items.length < this.props.maxItems; | |
} | |
addItem() { | |
console.log('[App@addItem]') | |
if (this.isAddItemEnabled) | |
this.props.addItem(); | |
} | |
submit() { | |
console.log('[App@submit]') | |
if (this.props.loading) return; | |
var values = this.refs.list.getWrappedInstance().values; | |
if (!values.length) return; | |
this.props.submit(values) | |
.catch(_.noop); | |
} | |
removeLastItem() { | |
console.log('[App@removeLastItem]') | |
var { removeItem, items } = this.props; | |
removeItem(_.last(items)); | |
} | |
render() { | |
const { items, removeItem, loading, submissions } = this.props; | |
return ( | |
<main> | |
<Header | |
addItem={this.addItem} | |
removeItem={this.removeLastItem} | |
submit={this.submit}> | |
</Header> | |
<section> | |
<List ref="list" addItem={this.addItem} /> | |
</section> | |
<footer> | |
<Submissions /> | |
</footer> | |
</main> | |
) | |
} | |
} | |
// CONNECT - STATE TO PROPS | |
function mapStateToProps(state, ownProps) { | |
const { items, config, loading, submissions } = state.emails; | |
var result = { | |
items, | |
maxItems: config.maxItems, | |
loading, | |
submissions, | |
}; | |
// override state with ownProps | |
return _.assign(result, ownProps); | |
}; | |
// CONNECT - DISPATCH TO PROPS | |
function mapDispatchToProps (dispatch, ownProps) { | |
// if ownProps has same keys then omit them in actions | |
var result = _.transform(actions, function(res, val, key){ | |
if (!(key in ownProps)) res[key] = val; | |
},{}); | |
result = bindActionCreators(result, dispatch); | |
// console.log('result', result) | |
return result; | |
}; | |
// COMPONENT - Root | |
class Root extends Component { | |
render() { | |
return ( | |
<Provider store={store}> | |
<App /> | |
</Provider> | |
) | |
} | |
} | |
// CONNECT COMPONENTS TO REDUX STORE | |
App = connect(mapStateToProps, mapDispatchToProps, null, {withRef: true })(App); | |
List = connect(mapStateToProps, mapDispatchToProps, null, {withRef: true })(List); | |
ListItem = connect(mapStateToProps, mapDispatchToProps, null, {withRef: true })(ListItem); | |
Submissions = connect(mapStateToProps, mapDispatchToProps)(Submissions); | |
// MAIN | |
function main() { | |
render(<Root />, $('#app')[0]); | |
} | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment