Skip to content

Instantly share code, notes, and snippets.

@milankinen
Created February 8, 2017 18:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save milankinen/1324c105c944978d7ad9ac7f69740f79 to your computer and use it in GitHub Desktop.
Save milankinen/1324c105c944978d7ad9ac7f69740f79 to your computer and use it in GitHub Desktop.
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