|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<title>From Redux to Elm</title> |
|
</head> |
|
<body> |
|
<div id="elm-root"></div> |
|
<div id="redux-root"></div> |
|
<div id="earlgrey-root"></div> |
|
<script> |
|
|
|
// # Not-that-far-from-actual-Redux setup |
|
|
|
// Redux in a nutshell |
|
const Reduxy = { |
|
createStore: (reducer, initialState) => { |
|
const fns = []; |
|
let state = initialState; |
|
return { |
|
dispatch: action => { |
|
state = reducer(state, action); |
|
fns.forEach(fn => fn(action)); |
|
}, |
|
getState: () => state, |
|
subscribe: fn => fns.push(fn), |
|
}; |
|
}, |
|
}; |
|
|
|
// Assuming Flux-Standard-Actions |
|
const reducer = (state = { who: "Stranger" }, action) => { |
|
switch (action.type) { |
|
case "SAY_HELLO": |
|
return { |
|
who: action.payload, |
|
}; |
|
default: |
|
return state; |
|
} |
|
}; |
|
|
|
const store = Reduxy.createStore(reducer); |
|
|
|
// The laziest DOM renderer ever :-) |
|
store.subscribe(action => { |
|
const { who } = store.getState(); |
|
|
|
document.getElementById("redux-root").innerHTML = |
|
`Redux: Hello ${who}!`; |
|
}); |
|
|
|
store.dispatch({ type: "SAY_HELLO", payload: "World" }); |
|
|
|
|
|
// # Elm setup |
|
const app = Elm.FromReduxToElm.embed(document.getElementById("elm-root")); |
|
app.ports.sayHello.send("World"); |
|
|
|
|
|
// # TEA in JS |
|
// This is our own silly The Elm Architecture 0.18 |
|
// implementation using Flux-Standard-Actions as messages |
|
// and `innerHTML` instead of using Virtual DOM for being |
|
// able to compare with Redux. Note also that we're |
|
// hand-waving effect handling, ports and subscriptions |
|
// here to keep the example somewhat succinct. Also be |
|
// aware that this code is hacked together to illustrate |
|
// how TEA might be doing it's magic under the hood. |
|
const Earlgrey = { |
|
Cmd: { |
|
none: Symbol("Earlgrey.Cmd.none"), |
|
}, |
|
Sub: { |
|
batch: (...subs) => { |
|
// In lack of a suitable type system we might just |
|
// do it like `redux-saga` and tag our `Sub`s for |
|
// later reference |
|
subs.forEach(sub => { |
|
sub["@@earlgrey-sub"] = true; |
|
}); |
|
return subs; |
|
}, |
|
none: Symbol("Earlgrey.Sub.none"), |
|
}, |
|
|
|
// This would be part of the core libraries. Most of the |
|
// usual functionality you'd expect already has a named |
|
// function readily available but if you need something |
|
// specific, like decoding a custom event, there are |
|
// `...withOptions` versions you can configure, i.e. |
|
// with JSON decoders. |
|
Mouse: { |
|
// Contains magic that captures, buffers and handles events |
|
// related to the user's mouse. |
|
downs: constructor => { |
|
// Flows all the buffered events into TEA using the |
|
// the user supplied constructor function |
|
//eventBuffer.map(position => constructor(position)); |
|
//moreMagicCleanup(); |
|
}, |
|
}, |
|
|
|
//beginnerProgram: ... |
|
program: ({ init, ports, subscriptions, update, view }) => { |
|
// Initial state |
|
let root = null; |
|
let queue = []; |
|
let model = init[0]; |
|
let intialCmd = init[1]; |
|
|
|
// This might be a tad simplified :-) |
|
const render = () => { |
|
const vdom = view(model); |
|
root.innerHTML = String(vdom); |
|
}; |
|
|
|
// Here the command would trigger some kind |
|
// of effect that eventually put something |
|
// into the queue and schedule render slices |
|
// accordingly. We just mock this by pretending |
|
// the effect happended and put something into |
|
// the queue ourselves |
|
const maybePushCmd = cmd => { |
|
if (cmd && (cmd !== Earlgrey.Cmd.none)) { |
|
queue.push(cmd); |
|
} |
|
}; |
|
|
|
const maybePushSub = sub => { |
|
if (sub && (sub !== Earlgrey.Sub.none)) { |
|
queue.push(sub); |
|
} |
|
}; |
|
|
|
// Reducing our message queue using `update` to |
|
// obtain the latest snapshot of the appplication |
|
// state a.k.a. the `model` in Elm |
|
const reduceQueue = () => { |
|
const newCmds = queue.map(msg => { |
|
const [newModel, cmd] = update(msg, model); |
|
model = newModel; |
|
return cmd; |
|
}).filter(Boolean); |
|
|
|
// We're done with this slice of work |
|
queue = []; |
|
|
|
// Queuing new commands created from the |
|
// `update` reducer step |
|
newCmds.forEach(maybePushCmd); |
|
|
|
// Now that we have our most recent model we |
|
// evaluate our subscriptions and push resulting |
|
// messages into our queue |
|
subscriptions(model).forEach(maybePushSub); |
|
}; |
|
|
|
// If we receive an initial Cmd, we queue that, |
|
// maybe we query our server on App startup? |
|
maybePushCmd(intialCmd); |
|
|
|
return { |
|
embed: domNode => { |
|
root = domNode; |
|
|
|
const thisPortSetup = {}; |
|
|
|
// Creating ports from our `actionCreator` |
|
// look-a-likes |
|
Object.keys(ports).forEach(portName => { |
|
thisPortSetup[portName] = { |
|
send: payload => { |
|
queue.push(ports[portName](payload)); |
|
|
|
reduceQueue(); |
|
Earlgrey.schedule(render); |
|
}, |
|
//subscribe: fn => ... |
|
}; |
|
}); |
|
|
|
// Scheduling the initial render |
|
Earlgrey.schedule(render); |
|
|
|
return { |
|
ports: thisPortSetup, |
|
}; |
|
}, |
|
//fullscreen: ... |
|
//worker: ... |
|
}; |
|
}, |
|
// Async scheduler with `setTimeout` fallback |
|
schedule: window.requestAnimationFrame |
|
? fn => window.requestAnimationFrame(fn) |
|
: fn => window.setTimeout(fn), |
|
}; |
|
|
|
// The constructor functions for `Sub`s are really just `actionCreator`s |
|
const subs = { |
|
letsHearEngage: () => ({ type: "LETS_HEAR_ENGAGE" }), |
|
captureMouse: position => ({ type: "MOUSE_DOWN", payload: position }), |
|
}; |
|
|
|
// The actual program in terms of TEA. It only consists of constants |
|
// and pure functions, Elm can enforce that with its type system, in |
|
// JS you need to rely on conventions... |
|
// |
|
// Note that Redux took inspiration from Elm so it's |
|
// actually not so strange that the setup seems rather |
|
// familiar. |
|
const Hot = Earlgrey.program({ |
|
// Provide the initial model and maybe an initial command |
|
// for the runtime to evaluate when the app is loaded |
|
init: [{ who: "Stranger" }, Earlgrey.Cmd.none], |
|
|
|
// We're hand-waving the setup but you can tell that |
|
// ports work a lot like `actionCreator`s |
|
ports: { |
|
sayHello: payload => ({ |
|
type: "SAY_HELLO", |
|
payload, |
|
}), |
|
}, |
|
|
|
// This isn't working in our mock setup but this tells TEA |
|
// what outside signals it should subscribe to |
|
subscriptions: model => Earlgrey.Sub.batch([ |
|
// You can put `Sub msg`s into the queue based on the current |
|
// model. JS is totally fine with dispatching stuff our program |
|
// doesn't know about, Elm makes sure that this doesn't happen |
|
model.who === "Picard" ? subs.letsHearEngage() : Earlgrey.Sub.none, |
|
|
|
// Interactions with the outside world can be channeled |
|
// into TEA via `Sub`s too, like `mousedown` events |
|
Earlgrey.Mouse.downs(subs.captureMouse), |
|
]), |
|
|
|
// Like Redux TEA uses a *pure* reducer function called `update` |
|
update: (msg, model) => { |
|
switch (msg.type) { |
|
case "SAY_HELLO": |
|
return [{ who: msg.payload }, Earlgrey.Cmd.none]; |
|
default: |
|
return [model, Earlgrey.Cmd.none]; |
|
} |
|
}, |
|
|
|
// No VDOM but you get the point: the view is just a *pure* |
|
// function from `Model` to a description of the view |
|
// you want TEA to display. If you're using `Platform.program` |
|
// there is no `view` and the only way to create an app |
|
// from this program is by calling `App.worker()` |
|
view: ({ who }) => `Earlgrey: Hello ${who}!`, |
|
}); |
|
|
|
// Go! |
|
const tea = Hot.embed(document.getElementById("earlgrey-root")); |
|
tea.ports.sayHello.send("World"); |
|
|
|
</script> |
|
</body> |
|
</html> |