Pekonimmmmehustelua
This file contains 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
import React from "react" | |
import Bacon from "baconjs" | |
import {render} from "react-dom" | |
import liftVDOM from "./liftVDOM" | |
import combineAsComponent from "./combineAsComponent" | |
import Atom from "./atom" | |
function initApp() => { | |
const bmiModels = Atom([BMIModel()]) | |
const bmiComponents = | |
bmiModels.map(models => models.map(m => <BMI model={m} />)) | |
// just for fun | |
bmiModels | |
.map(models => models.map(Bacon.combineTemplate)) | |
.flatMapLatest(Bacon.combineAsArray) | |
.log("all BMIs") | |
return liftVDOM( | |
<div> | |
<h1>BMI counterz</h1> | |
<div>{bmiComponents}</div> | |
<hr /> | |
<button onClick={() => bmiModels.swap(models => [...models, BMIModel()])}> | |
Add | |
</button> | |
</div> | |
) | |
} | |
const BMIModel = () => { | |
const weight = Atom(70) | |
const height = Atom(170) | |
const bmiP = Bacon.combineWith(weight, height, (w, h) => Math.round(w/(h * h * 0.0001))) | |
return {weight, height, bmiP} | |
} | |
const bmi = ({weight, height, bmiP}) => | |
liftVDOM( | |
<div> | |
<hr /> | |
{slider("Weight", "kg", 40, 140, weight)} | |
{slider("Height", "cm", 140, 210, height)} | |
BMI: {bmiP} | |
</div> | |
) | |
const BMI = combineAsComponent(({model}) => model.flatMapLatest(bmi)) | |
const slider = (title, units, min, max, value) => | |
liftVDOM( | |
<div> | |
{title}: {value} {units} | |
<div> | |
<input type="range" min={min} max={max} value={value} | |
onChange={e => value.reset(e.target.value)}/> | |
</div> | |
</div> | |
) | |
initApp().onValue(app => render(app, document.getElementById("app"))) |
This file contains 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
import Bacon from "baconjs" | |
export default function Atom(initialValue) { | |
const bus = new Bacon.Bus() | |
const atom = bus.scan(initialValue, (state, fn) => fn(state)) | |
atom.reset = val => bus.push(_ => val) | |
atom.swap = fn => bus.push(fn) | |
return atom | |
} |
This file contains 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
import Bacon from "baconjs" | |
import React from "react" | |
import {keys, values, zip, zipObject} from "lodash" | |
/** | |
* Wraps Observable render function [(...args) => Observable] into React.Component so that | |
* properties that are passed from parent elements are transformed into EventStreams | |
* (e.g. (prop1, prop2) => (Observable(prop1), Observable(prop2)). | |
* | |
* Returned React.Component can be used like any other React.Component | |
* | |
* Example: | |
* (see liftVDOM first) | |
* | |
* const App = React.createClass({ | |
* render() { | |
* const {width, height} = this.state // change this somehow | |
* return ( | |
* <div> | |
* Example | |
* <BMI width={width} height={height} /> | |
* </div> | |
* ) | |
* } | |
* }) | |
* | |
* const BMI = combineAsComponent({width, height} => { | |
* const bmiP = Bacon.combineWith(weight, height, (w, h) => Math.round(w/(h * h * 0.0001))) | |
* return liftVDOM( | |
* <div>BMI for {width} and {height} is {bmiP}</div> | |
* ) | |
* }) | |
*/ | |
export default function combineAsComponent(renderFn) { | |
return React.createClass({ | |
getInitialState() { | |
const propsBuses = zipObject(keys(this.props), values(this.props).map(val => new Bacon.Bus())) | |
const propsS = | |
zipObject(keys(this.props), zip(values(propsBuses), values(this.props)).map(([bus, initial]) => ( | |
bus.startWith(initial).skipDuplicates() | |
))) | |
return { | |
propsBuses, | |
vdomS: renderFn(propsS), | |
vdom: null | |
} | |
}, | |
componentWillMount() { | |
const updateVDOM = vdom => this.setState({vdom}) | |
if (process.browser) { | |
this.setState({ dispose: this.state.vdomS.onValue(updateVDOM) }) | |
} else { | |
this.state.vdomS.take(1).onValue(updateVDOM) | |
} | |
}, | |
componentWillReceiveProps(nextProps) { | |
keys(nextProps).forEach(propName => { | |
const bus = this.state.propsBuses[propName] | |
if (!bus) { | |
console.warn( | |
`Trying to pass property "${propName}" that is not set during the component creation.`, | |
`Ignoring this property.` | |
) | |
} else { | |
bus.push(nextProps[propName]) | |
} | |
}) | |
}, | |
shouldComponentUpdate(nextProps, nextState) { | |
return nextState.vdom !== this.state.vdom | |
}, | |
componentWillUnmount() { | |
const {dispose} = this.state | |
if (dispose) { | |
dispose() | |
} | |
}, | |
render() { | |
return this.state.vdom | |
} | |
}) | |
} |
This file contains 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
import Bacon from "baconjs" | |
import React from "react" | |
import {cloneAndReplaceProps, isValidElement} from "react/lib/ReactElement" | |
import {find, isEmpty, isArray} from "lodash" | |
/** | |
* Transforms: VDOM(Observable) => Observable(VDOM) | |
* Example: | |
* const a = Bacon.constant(4) | |
* const b = Bacon.constant(3) | |
* const c = Bacon.combineWith(a, b, (a, b) => a + b) | |
* | |
* liftVDOM(<div>{a} + {b} = <span className="result">{c}</span></div>) | |
*/ | |
export default function liftVDOM(vdom) { | |
const obs = resolveObservables(vdom, []) | |
if (isEmpty(obs)) { | |
return Bacon.constant(vdom) | |
} else { | |
return Bacon | |
.combineAsArray(obs) | |
.map(values => assignObservableValues(vdom, values)) | |
} | |
function resolveObservables(el, obs) { | |
const propKeys = Object.keys(el.props || {}) | |
for (let k = 0 ; k < propKeys.length ; k++) { | |
const key = propKeys[k] | |
const prop = el.props[key] | |
if (key === "children") { | |
const children = isArray(prop) ? prop : [ prop ] | |
for (let i = 0 ; i < children.length ; i++) { | |
const child = children[i] | |
if (child instanceof Bacon.Observable && !find(obs, o => o.obs === child)) { | |
obs.push(child.map(value => ({obs: child, value}))) | |
} else if (isValidElement(child)) { | |
resolveObservables(child, obs) | |
} | |
} | |
} else { | |
if (prop instanceof Bacon.Observable && !find(obs, o => o.obs === prop)) { | |
obs.push(prop.map(value => ({obs: prop, value}))) | |
} | |
} | |
} | |
return obs | |
} | |
function assignObservableValues(el, obsValues) { | |
const newProps = {} | |
const propKeys = Object.keys(el.props || {}) | |
for (let k = 0 ; k < propKeys.length ; k++) { | |
const key = propKeys[k] | |
const prop = el.props[key] | |
if (key === "children") { | |
const children = isArray(prop) ? prop : [ prop ] | |
const newChildren = [] | |
for (let i = 0 ; i < children.length ; i++) { | |
const child = children[i] | |
if (child instanceof Bacon.Observable) { | |
newChildren.push(find(obsValues, r => r.obs === child).value) | |
} else if (isValidElement(child)) { | |
newChildren.push(assignObservableValues(child, obsValues)) | |
} else { | |
newChildren.push(child) | |
} | |
} | |
newProps.children = isArray(prop) ? newChildren : newChildren[0] | |
} else { | |
if (prop instanceof Bacon.Observable) { | |
newProps[key] = find(obsValues, r => r.obs === prop).value | |
} else { | |
newProps[key] = prop | |
} | |
} | |
} | |
return cloneAndReplaceProps(el, newProps) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment