Skip to content

Instantly share code, notes, and snippets.

@ronen-e
Created June 23, 2018 19:48
Show Gist options
  • Save ronen-e/efc469276d827a6121f0d2b6765102be to your computer and use it in GitHub Desktop.
Save ronen-e/efc469276d827a6121f0d2b6765102be to your computer and use it in GitHub Desktop.
Simple react-redux demo with translations for Browser
header {
margin-top: 20px;
display: flex;
justify-content: space-around;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://unpkg.com/@reactivex/rxjs@5.0.3/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>
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