CULLI state (de)composition
import * as O from "most" | |
import * as L from "partial.lenses" | |
import DOM from "@culli/dom" | |
import Store, {Memory, byType} from "@culli/store" | |
import {run} from "@cycle/most-run" | |
// partial.lenses lens => culli lens | |
const P = (pl) => ({ | |
get: L.get(pl), | |
set: L.set(pl) | |
}) | |
run(App, { | |
DOM: DOM("#app"), | |
Store: Store(Memory({ | |
text: "tsers!", | |
notSoNestedState: 10, | |
some: { | |
nested: { | |
state: 1 | |
} | |
} | |
})) | |
}) | |
function App(sources) { | |
function model(state) { | |
// we haven't decomposed the "root state" yet, so we must modify | |
// text property | |
const dispatch = state.actions.reduce(byType({ | |
["SET_TEXT"]: (state, newText) => ({...state, text: newText}) | |
})) | |
// however, the exactly identical behaviour could have been achieved | |
// with the following code: | |
// const dispatch = state.value.select("text").actions.reduce((text, {payload: newText}) => newText) | |
// now let's "decompose state into smaller parts now" | |
const text = state.value.select("text") | |
const counters = state.value.select(P(L.pick({ | |
counter1: "notSoNestedState", | |
counter2: ["some", "nested", "state"] | |
}))) | |
return { | |
dispatch, | |
props: { counters, text, entireState: state } | |
} | |
} | |
function view({counters, text, entireState}) { | |
// now let's pass the decomposed "counters" to our "general-purpose" component | |
// that expects {counter1, counter2} | |
const children = Counters({...sources, Store: counters}) | |
const vdom = h("div", [ | |
h("div", [ | |
h("h1", "Entire state is:"), | |
h("pre", [entireState.value.map(s => JSON.stringify(s, null, 2))]) | |
]), | |
h("label", [ | |
"Some text: ", | |
h("input.text", {type: "text", value: text.value}) | |
]), | |
h("div", [ | |
children.DOM | |
]), | |
]) | |
return {vdom, children} | |
} | |
function intent(vdom) { | |
return vdom.on(".text", "input").map(e => ({type: "SET_TEXT", payload: e.target.value})) | |
} | |
const {DOM: {h, combine}, Store} = sources | |
const {dispatch, props} = model(Store) | |
const {vdom, children} = view(props) | |
const actions = intent(vdom) | |
return { | |
DOM: combine(vdom), | |
// and here we "compose" actions by using dispatch function and normal merge operator | |
Store: O.merge(dispatch(actions), children.Store) | |
} | |
} | |
function Counters(sources) { | |
const {DOM: {h, combine}, Store: state} = sources | |
// and here again, we can use the state and "decompose" it again and don't worry much... | |
const a = state.value.select("counter1") | |
const b = state.value.select("counter2") | |
// ...and pass those decomposed states to child components :-) | |
const counterA = Counter({...sources, title: "A", Store: a}) | |
const counterB = Counter({...sources, title: "B", Store: b}) | |
return { | |
DOM: combine(h("div", [ | |
h("h1", "Counters state:"), | |
h("pre", [state.value.map(s => JSON.stringify(s, null, 2))]), | |
h("div", [ | |
counterA.DOM, | |
counterB.DOM | |
]) | |
])), | |
// ...and again compose mods into bigger one | |
Store: O.merge(counterA.Store, counterB.Store) | |
} | |
} | |
function Counter({title, DOM: {h, combine}, Store: state}) { | |
// ...and again, in order to make state modifications, we must use reduce + dispatch | |
const dispatch = state.actions.reduce(byType({ | |
["INC"]: state => state + 1, | |
["DEC"]: state => state - 1 | |
})) | |
const vdom = h("div", [ | |
h("h2", ["Counter ", title, " (", state.value, ")"]), | |
h("button.inc", "+"), | |
h("button.dec", "-") | |
]) | |
const actions = O.merge( | |
vdom.on(".inc", "click").map(() => ({type: "INC"})), | |
vdom.on(".dec", "click").map(() => ({type: "DEC"})) | |
) | |
return { | |
DOM: combine(vdom), | |
Store: dispatch(actions) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment