Created
July 7, 2016 18:50
-
-
Save anonymous/7283f16ad512d6901bec67d26517fff3 to your computer and use it in GitHub Desktop.
JS Bin // source http://jsbin.com/jozihiluki
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"> | |
<meta name="viewport" content="width=device-width"> | |
<title>JS Bin</title> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.5.2/redux.min.js"></script> | |
<script src="https://wzrd.in/standalone/reduce-reducers"></script> | |
<style id="jsbin-css"> | |
#tabs span { | |
border: 1px solid black; | |
border-radius: 7px 7px 0 0; | |
padding: 3px; | |
cursor: pointer; | |
} | |
#tabs span.active { | |
background-color: lightgray; | |
} | |
#year input { | |
width: 40px; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="tabs"></div> | |
<p id="year"> | |
Year: | |
<select> | |
</select> | |
<button>Set</button> | |
</p> | |
<p> | |
<span>foo:</span> | |
<input id="draggable-chart" type="range" min="-50" max="150" value="25"/> | |
<span id="draggable-value"></span> | |
</p> | |
<p> | |
<span>total:</span> | |
<span id="total-value"></span> | |
</p> | |
<script id="jsbin-javascript"> | |
/* jshint esnext: true */ | |
/* jshint asi: true */ | |
/* jshint laxcomma: true */ | |
/* | |
ES6: (state = [], action) => { ... } | |
ES5: function(state, action) { if (!state) state = []; ... } | |
Functions with a single expression using ES6 arrow functions don't need {}: | |
[1,2,3].map(elem => elem * 2) // returns [2,4,6] | |
The const keyword declares a constant. Reusing the same name will throw an error: | |
const foo = 'bar' | |
const foo = 'baz' // throws "SyntaxError: Identifier 'foo' has already been declared" | |
`template '${foo}' string` // equivalent to: "template 'bar' string" | |
*/ | |
const { createStore, combineReducers } = Redux; | |
const extractId = (id) => parseInt(id.match(/\d+/g)[0]) // eg. "model12" => 12 | |
/* * * * * * * | |
* SELECTORS * | |
* * * * * * */ | |
const draggableChart = document.getElementById('draggable-chart') | |
, draggableValue = document.getElementById('draggable-value') | |
, totalValue = document.getElementById('total-value') | |
const selectYear = document.querySelector('#year select') | |
, setYear = document.querySelector('#year button') | |
/* * * * * * * | |
* REDUCERS * | |
* * * * * * */ | |
// Reducers are just like [].reduce | |
// The accumulator is the state, and the action is the next value | |
const addForecastModel = (state, action) => { | |
return state.map(e => { e.active = false; return e }).concat({ | |
id: forecastId++, | |
active: true, | |
defaultYear: action.defaultYear || state[0].defaultYear, | |
drivers: !state[0] ? action.drivers : | |
Object.assign({}, state[0].drivers, action.drivers || {}) | |
}) | |
} | |
const updateForecastModel = (state, action) => { | |
const modelIndex = state.findIndex(e => e.active) | |
return state.slice(0, modelIndex) | |
.concat([{ | |
id: state[modelIndex].id, | |
active: true, | |
defaultYear: action.defaultYear || | |
state[modelIndex].defaultYear, | |
drivers: Object.assign( {} | |
, state[0].drivers | |
, action.drivers || {} ) | |
}]) | |
.concat(state.slice(modelIndex + 1)) | |
} | |
/** FORECASTMODEL REDUCER **/ | |
var forecastId = 0; | |
const forecastModel = (state = [], action) => { | |
switch (action.type) { | |
case 'ADD_OR_UPDATE_MODEL': | |
const activeModel = state.find(e => e.active) | |
if (activeModel && activeModel.id === 0) | |
return addForecastModel(state, action) | |
else if (activeModel) | |
return updateForecastModel(state, action) | |
else | |
return state | |
break | |
case 'ADD_MODEL': | |
return addForecastModel(state, action) | |
case 'SELECT_MODEL': | |
return state.map(e => { | |
e.active = e.id === action.id ? true : false | |
return e | |
}) | |
case 'UPDATE_MODEL': | |
return updateForecastModel(state, action) | |
case 'DELETE_MODEL': | |
return modelIndex === 0 ? | |
state : state.slice(0, modelIndex).concat(state.slice(modelIndex + 1)) | |
default: | |
return state | |
} | |
} | |
/** SELECTEDYEAR REDUCER **/ | |
const selectedYear = (state = [], action) => { | |
switch (action.type) { | |
case 'SELECT_YEAR': | |
return state.map(e => | |
!e.active ? e : Object.assign({}, e, { selectedYear: action.year })) | |
default: | |
return state.map(e => | |
!e.active ? e : Object.assign({}, e, { selectedYear: e.defaultYear })) | |
} | |
} | |
/** TREFISAPP REDUCER **/ | |
const trefisApp = reduceReducers(forecastModel, selectedYear) | |
// The store lets you dispatch actions and subscribe to them for rendering | |
const trefisStore = createStore(trefisApp) | |
/* * * * * * * * | |
* RENDER VIEW * | |
* * * * * * * */ | |
// A function bound to tabs to select a tab/model | |
function selectModel(event) { | |
const modelId = extractId(event.target.id) | |
trefisStore.dispatch({ | |
type: 'SELECT_MODEL', | |
id: modelId | |
}) | |
} | |
const renderTabs = () => { | |
const tabs = trefisStore.getState().map(e => | |
`<span id="model${e.id}" class="${e.active ? 'active' : ''}" onClick="selectModel(event)">` + | |
`model${e.id}` + | |
`</span>` | |
).join(' ') | |
document.getElementById('tabs').innerHTML = tabs | |
} | |
const renderYearDropdown = (baseModel, activeModel) => { | |
const years = [0,1,2,3,4].map(e => baseModel.defaultYear + e) | |
.map(e => `<option value="${e}">${e}</option>`) | |
.join('') | |
selectYear.innerHTML = years | |
selectYear.selectedIndex = activeModel.selectedYear - baseModel.defaultYear | |
} | |
const renderModelValues = (baseModel, activeModel) => { | |
const growthFn = (total) => | |
(total * (Math.log(activeModel.selectedYear - baseModel.defaultYear + 1) + 1)) | |
draggableChart.value = activeModel.drivers.foo | |
draggableValue.innerHTML = activeModel.drivers.foo | |
totalValue.innerHTML = growthFn(activeModel.drivers.total()) | |
} | |
const toggleEditYear = (activeModel) => { | |
if (activeModel.id === 0) | |
setYear.setAttribute('disabled', 'disabled') | |
else | |
setYear.removeAttribute('disabled') | |
} | |
// Renders the forecast model view on each forecastModel state change | |
trefisStore.subscribe(() => { | |
console.log(trefisStore.getState()) | |
const activeModel = trefisStore.getState().find(e => e.active) | |
, baseModel = trefisStore.getState()[0] | |
renderTabs() | |
renderModelValues(baseModel, activeModel) | |
toggleEditYear(activeModel) | |
renderYearDropdown(baseModel, activeModel) | |
}) | |
/* * * * * * * * | |
* INITIALIZE * | |
* * * * * * * */ | |
// Initialize the base model | |
trefisStore.dispatch({ | |
type: 'ADD_MODEL', | |
active: true, | |
defaultYear: 2016, | |
drivers: { | |
foo: 22, | |
total: function() { return this.foo * 3 } | |
} | |
}) | |
/* * * * * * * * * * | |
* EVENT LISTENERS * | |
* * * * * * * * * */ | |
// Draggable chart arm is let go | |
draggableChart.addEventListener('mouseup', (event) => { | |
trefisStore.dispatch({ | |
type: 'ADD_OR_UPDATE_MODEL', | |
drivers: { foo: parseInt(draggableChart.value) } | |
}) | |
}) | |
// Year is changed in the dropdown | |
selectYear.addEventListener('change', (event) => { | |
trefisStore.dispatch({ | |
type: 'SELECT_YEAR', | |
year: parseInt(event.target.value) | |
}) | |
}) | |
// The set year button is clicked | |
setYear.addEventListener('click', (event) => { | |
var selectedYear = selectYear.options[selectYear.selectedIndex].value | |
trefisStore.dispatch({ | |
type: 'UPDATE_MODEL', | |
defaultYear: parseInt(selectedYear), | |
drivers: { | |
foo: parseInt(draggableChart.value) | |
} | |
}) | |
}) | |
</script> | |
<script id="jsbin-source-css" type="text/css">#tabs span { | |
border: 1px solid black; | |
border-radius: 7px 7px 0 0; | |
padding: 3px; | |
cursor: pointer; | |
} | |
#tabs span.active { | |
background-color: lightgray; | |
} | |
#year input { | |
width: 40px; | |
}</script> | |
<script id="jsbin-source-javascript" type="text/javascript">/* jshint esnext: true */ | |
/* jshint asi: true */ | |
/* jshint laxcomma: true */ | |
/* | |
ES6: (state = [], action) => { ... } | |
ES5: function(state, action) { if (!state) state = []; ... } | |
Functions with a single expression using ES6 arrow functions don't need {}: | |
[1,2,3].map(elem => elem * 2) // returns [2,4,6] | |
The const keyword declares a constant. Reusing the same name will throw an error: | |
const foo = 'bar' | |
const foo = 'baz' // throws "SyntaxError: Identifier 'foo' has already been declared" | |
`template '${foo}' string` // equivalent to: "template 'bar' string" | |
*/ | |
const { createStore, combineReducers } = Redux; | |
const extractId = (id) => parseInt(id.match(/\d+/g)[0]) // eg. "model12" => 12 | |
/* * * * * * * | |
* SELECTORS * | |
* * * * * * */ | |
const draggableChart = document.getElementById('draggable-chart') | |
, draggableValue = document.getElementById('draggable-value') | |
, totalValue = document.getElementById('total-value') | |
const selectYear = document.querySelector('#year select') | |
, setYear = document.querySelector('#year button') | |
/* * * * * * * | |
* REDUCERS * | |
* * * * * * */ | |
// Reducers are just like [].reduce | |
// The accumulator is the state, and the action is the next value | |
const addForecastModel = (state, action) => { | |
return state.map(e => { e.active = false; return e }).concat({ | |
id: forecastId++, | |
active: true, | |
defaultYear: action.defaultYear || state[0].defaultYear, | |
drivers: !state[0] ? action.drivers : | |
Object.assign({}, state[0].drivers, action.drivers || {}) | |
}) | |
} | |
const updateForecastModel = (state, action) => { | |
const modelIndex = state.findIndex(e => e.active) | |
return state.slice(0, modelIndex) | |
.concat([{ | |
id: state[modelIndex].id, | |
active: true, | |
defaultYear: action.defaultYear || | |
state[modelIndex].defaultYear, | |
drivers: Object.assign( {} | |
, state[0].drivers | |
, action.drivers || {} ) | |
}]) | |
.concat(state.slice(modelIndex + 1)) | |
} | |
/** FORECASTMODEL REDUCER **/ | |
var forecastId = 0; | |
const forecastModel = (state = [], action) => { | |
switch (action.type) { | |
case 'ADD_OR_UPDATE_MODEL': | |
const activeModel = state.find(e => e.active) | |
if (activeModel && activeModel.id === 0) | |
return addForecastModel(state, action) | |
else if (activeModel) | |
return updateForecastModel(state, action) | |
else | |
return state | |
break | |
case 'ADD_MODEL': | |
return addForecastModel(state, action) | |
case 'SELECT_MODEL': | |
return state.map(e => { | |
e.active = e.id === action.id ? true : false | |
return e | |
}) | |
case 'UPDATE_MODEL': | |
return updateForecastModel(state, action) | |
case 'DELETE_MODEL': | |
return modelIndex === 0 ? | |
state : state.slice(0, modelIndex).concat(state.slice(modelIndex + 1)) | |
default: | |
return state | |
} | |
} | |
/** SELECTEDYEAR REDUCER **/ | |
const selectedYear = (state = [], action) => { | |
switch (action.type) { | |
case 'SELECT_YEAR': | |
return state.map(e => | |
!e.active ? e : Object.assign({}, e, { selectedYear: action.year })) | |
default: | |
return state.map(e => | |
!e.active ? e : Object.assign({}, e, { selectedYear: e.defaultYear })) | |
} | |
} | |
/** TREFISAPP REDUCER **/ | |
const trefisApp = reduceReducers(forecastModel, selectedYear) | |
// The store lets you dispatch actions and subscribe to them for rendering | |
const trefisStore = createStore(trefisApp) | |
/* * * * * * * * | |
* RENDER VIEW * | |
* * * * * * * */ | |
// A function bound to tabs to select a tab/model | |
function selectModel(event) { | |
const modelId = extractId(event.target.id) | |
trefisStore.dispatch({ | |
type: 'SELECT_MODEL', | |
id: modelId | |
}) | |
} | |
const renderTabs = () => { | |
const tabs = trefisStore.getState().map(e => | |
`<span id="model${e.id}" class="${e.active ? 'active' : ''}" onClick="selectModel(event)">` + | |
`model${e.id}` + | |
`</span>` | |
).join(' ') | |
document.getElementById('tabs').innerHTML = tabs | |
} | |
const renderYearDropdown = (baseModel, activeModel) => { | |
const years = [0,1,2,3,4].map(e => baseModel.defaultYear + e) | |
.map(e => `<option value="${e}">${e}</option>`) | |
.join('') | |
selectYear.innerHTML = years | |
selectYear.selectedIndex = activeModel.selectedYear - baseModel.defaultYear | |
} | |
const renderModelValues = (baseModel, activeModel) => { | |
const growthFn = (total) => | |
(total * (Math.log(activeModel.selectedYear - baseModel.defaultYear + 1) + 1)) | |
draggableChart.value = activeModel.drivers.foo | |
draggableValue.innerHTML = activeModel.drivers.foo | |
totalValue.innerHTML = growthFn(activeModel.drivers.total()) | |
} | |
const toggleEditYear = (activeModel) => { | |
if (activeModel.id === 0) | |
setYear.setAttribute('disabled', 'disabled') | |
else | |
setYear.removeAttribute('disabled') | |
} | |
// Renders the forecast model view on each forecastModel state change | |
trefisStore.subscribe(() => { | |
console.log(trefisStore.getState()) | |
const activeModel = trefisStore.getState().find(e => e.active) | |
, baseModel = trefisStore.getState()[0] | |
renderTabs() | |
renderModelValues(baseModel, activeModel) | |
toggleEditYear(activeModel) | |
renderYearDropdown(baseModel, activeModel) | |
}) | |
/* * * * * * * * | |
* INITIALIZE * | |
* * * * * * * */ | |
// Initialize the base model | |
trefisStore.dispatch({ | |
type: 'ADD_MODEL', | |
active: true, | |
defaultYear: 2016, | |
drivers: { | |
foo: 22, | |
total: function() { return this.foo * 3 } | |
} | |
}) | |
/* * * * * * * * * * | |
* EVENT LISTENERS * | |
* * * * * * * * * */ | |
// Draggable chart arm is let go | |
draggableChart.addEventListener('mouseup', (event) => { | |
trefisStore.dispatch({ | |
type: 'ADD_OR_UPDATE_MODEL', | |
drivers: { foo: parseInt(draggableChart.value) } | |
}) | |
}) | |
// Year is changed in the dropdown | |
selectYear.addEventListener('change', (event) => { | |
trefisStore.dispatch({ | |
type: 'SELECT_YEAR', | |
year: parseInt(event.target.value) | |
}) | |
}) | |
// The set year button is clicked | |
setYear.addEventListener('click', (event) => { | |
var selectedYear = selectYear.options[selectYear.selectedIndex].value | |
trefisStore.dispatch({ | |
type: 'UPDATE_MODEL', | |
defaultYear: parseInt(selectedYear), | |
drivers: { | |
foo: parseInt(draggableChart.value) | |
} | |
}) | |
})</script></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
#tabs span { | |
border: 1px solid black; | |
border-radius: 7px 7px 0 0; | |
padding: 3px; | |
cursor: pointer; | |
} | |
#tabs span.active { | |
background-color: lightgray; | |
} | |
#year input { | |
width: 40px; | |
} |
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
/* jshint esnext: true */ | |
/* jshint asi: true */ | |
/* jshint laxcomma: true */ | |
/* | |
ES6: (state = [], action) => { ... } | |
ES5: function(state, action) { if (!state) state = []; ... } | |
Functions with a single expression using ES6 arrow functions don't need {}: | |
[1,2,3].map(elem => elem * 2) // returns [2,4,6] | |
The const keyword declares a constant. Reusing the same name will throw an error: | |
const foo = 'bar' | |
const foo = 'baz' // throws "SyntaxError: Identifier 'foo' has already been declared" | |
`template '${foo}' string` // equivalent to: "template 'bar' string" | |
*/ | |
const { createStore, combineReducers } = Redux; | |
const extractId = (id) => parseInt(id.match(/\d+/g)[0]) // eg. "model12" => 12 | |
/* * * * * * * | |
* SELECTORS * | |
* * * * * * */ | |
const draggableChart = document.getElementById('draggable-chart') | |
, draggableValue = document.getElementById('draggable-value') | |
, totalValue = document.getElementById('total-value') | |
const selectYear = document.querySelector('#year select') | |
, setYear = document.querySelector('#year button') | |
/* * * * * * * | |
* REDUCERS * | |
* * * * * * */ | |
// Reducers are just like [].reduce | |
// The accumulator is the state, and the action is the next value | |
const addForecastModel = (state, action) => { | |
return state.map(e => { e.active = false; return e }).concat({ | |
id: forecastId++, | |
active: true, | |
defaultYear: action.defaultYear || state[0].defaultYear, | |
drivers: !state[0] ? action.drivers : | |
Object.assign({}, state[0].drivers, action.drivers || {}) | |
}) | |
} | |
const updateForecastModel = (state, action) => { | |
const modelIndex = state.findIndex(e => e.active) | |
return state.slice(0, modelIndex) | |
.concat([{ | |
id: state[modelIndex].id, | |
active: true, | |
defaultYear: action.defaultYear || | |
state[modelIndex].defaultYear, | |
drivers: Object.assign( {} | |
, state[0].drivers | |
, action.drivers || {} ) | |
}]) | |
.concat(state.slice(modelIndex + 1)) | |
} | |
/** FORECASTMODEL REDUCER **/ | |
var forecastId = 0; | |
const forecastModel = (state = [], action) => { | |
switch (action.type) { | |
case 'ADD_OR_UPDATE_MODEL': | |
const activeModel = state.find(e => e.active) | |
if (activeModel && activeModel.id === 0) | |
return addForecastModel(state, action) | |
else if (activeModel) | |
return updateForecastModel(state, action) | |
else | |
return state | |
break | |
case 'ADD_MODEL': | |
return addForecastModel(state, action) | |
case 'SELECT_MODEL': | |
return state.map(e => { | |
e.active = e.id === action.id ? true : false | |
return e | |
}) | |
case 'UPDATE_MODEL': | |
return updateForecastModel(state, action) | |
case 'DELETE_MODEL': | |
return modelIndex === 0 ? | |
state : state.slice(0, modelIndex).concat(state.slice(modelIndex + 1)) | |
default: | |
return state | |
} | |
} | |
/** SELECTEDYEAR REDUCER **/ | |
const selectedYear = (state = [], action) => { | |
switch (action.type) { | |
case 'SELECT_YEAR': | |
return state.map(e => | |
!e.active ? e : Object.assign({}, e, { selectedYear: action.year })) | |
default: | |
return state.map(e => | |
!e.active ? e : Object.assign({}, e, { selectedYear: e.defaultYear })) | |
} | |
} | |
/** TREFISAPP REDUCER **/ | |
const trefisApp = reduceReducers(forecastModel, selectedYear) | |
// The store lets you dispatch actions and subscribe to them for rendering | |
const trefisStore = createStore(trefisApp) | |
/* * * * * * * * | |
* RENDER VIEW * | |
* * * * * * * */ | |
// A function bound to tabs to select a tab/model | |
function selectModel(event) { | |
const modelId = extractId(event.target.id) | |
trefisStore.dispatch({ | |
type: 'SELECT_MODEL', | |
id: modelId | |
}) | |
} | |
const renderTabs = () => { | |
const tabs = trefisStore.getState().map(e => | |
`<span id="model${e.id}" class="${e.active ? 'active' : ''}" onClick="selectModel(event)">` + | |
`model${e.id}` + | |
`</span>` | |
).join(' ') | |
document.getElementById('tabs').innerHTML = tabs | |
} | |
const renderYearDropdown = (baseModel, activeModel) => { | |
const years = [0,1,2,3,4].map(e => baseModel.defaultYear + e) | |
.map(e => `<option value="${e}">${e}</option>`) | |
.join('') | |
selectYear.innerHTML = years | |
selectYear.selectedIndex = activeModel.selectedYear - baseModel.defaultYear | |
} | |
const renderModelValues = (baseModel, activeModel) => { | |
const growthFn = (total) => | |
(total * (Math.log(activeModel.selectedYear - baseModel.defaultYear + 1) + 1)) | |
draggableChart.value = activeModel.drivers.foo | |
draggableValue.innerHTML = activeModel.drivers.foo | |
totalValue.innerHTML = growthFn(activeModel.drivers.total()) | |
} | |
const toggleEditYear = (activeModel) => { | |
if (activeModel.id === 0) | |
setYear.setAttribute('disabled', 'disabled') | |
else | |
setYear.removeAttribute('disabled') | |
} | |
// Renders the forecast model view on each forecastModel state change | |
trefisStore.subscribe(() => { | |
console.log(trefisStore.getState()) | |
const activeModel = trefisStore.getState().find(e => e.active) | |
, baseModel = trefisStore.getState()[0] | |
renderTabs() | |
renderModelValues(baseModel, activeModel) | |
toggleEditYear(activeModel) | |
renderYearDropdown(baseModel, activeModel) | |
}) | |
/* * * * * * * * | |
* INITIALIZE * | |
* * * * * * * */ | |
// Initialize the base model | |
trefisStore.dispatch({ | |
type: 'ADD_MODEL', | |
active: true, | |
defaultYear: 2016, | |
drivers: { | |
foo: 22, | |
total: function() { return this.foo * 3 } | |
} | |
}) | |
/* * * * * * * * * * | |
* EVENT LISTENERS * | |
* * * * * * * * * */ | |
// Draggable chart arm is let go | |
draggableChart.addEventListener('mouseup', (event) => { | |
trefisStore.dispatch({ | |
type: 'ADD_OR_UPDATE_MODEL', | |
drivers: { foo: parseInt(draggableChart.value) } | |
}) | |
}) | |
// Year is changed in the dropdown | |
selectYear.addEventListener('change', (event) => { | |
trefisStore.dispatch({ | |
type: 'SELECT_YEAR', | |
year: parseInt(event.target.value) | |
}) | |
}) | |
// The set year button is clicked | |
setYear.addEventListener('click', (event) => { | |
var selectedYear = selectYear.options[selectYear.selectedIndex].value | |
trefisStore.dispatch({ | |
type: 'UPDATE_MODEL', | |
defaultYear: parseInt(selectedYear), | |
drivers: { | |
foo: parseInt(draggableChart.value) | |
} | |
}) | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment