Om is awesome. oHm is a hommage to Om in GHCJS using Haskell's pipes, mvc and pipes-concurrent libraries.
Ohm at its core is the idea of building an application as a pure left fold over a stream of events. At a previous position we built a UI that captured this model in clojurescript and Om, this is a port of that architectural idea to Haskell.
-
Models
Models are the state of your application. Here's the Model from the todo mvc example mentioned later:
-
State
data ToDo = ToDo { _items :: [Item] , _editText :: String , _filter :: Filter } deriving ShowIn addition to the model you also need a updating function of type
mdlEvent -> model -> modelwhich is a left fold function that applies a to the Model resulting in the new Model. This function is one of the things you need to construct a . -
Here's the model function from our todo mvc example:
process :: Action -> ToDo -> ToDo process (NewItem str) todo = todo &~ do items %= (Item str False:) editText .= "" process (RemoveItem idx) todo = todo & items %~ deleteAt idx process (SetEditText str) todo = todo & editText .~ str process (SetCompleted idx c) todo = todo & items.element idx.completed .~ c process (SetFilter f) todo = todo & filter .~ fNote that MVC, one of the libraries that oHm is built on has a concept of a model too. In MVC Model refers to the pure transformation that happens within a Pipe and applies an event to the state to produce a new state. In oHm construction of an MVC Model happens with the
appModelfunction that therunComponentfunction applies for you.
-
-
Model Events
Model Events represent events that happen in your domain to effect change to the state of the world. This is the
Actiontype mentioned in the event -> model -> model function earlier:data Action = NewItem String | RemoveItem Index | SetEditText String | SetCompleted Index Bool | SetFilter Filter -
UI Events
UI Events occur at the points of interaction between user and your app. These are the sorts of things that you'd attach callbacks to: changes, clicks, mouse moves etc. A
DOMEventtype is provided to 1.1.2.5 for these events.For simpler apps, like our todo mvc example, the UI could emit events which are passed straight through to the model.
-
Processors
Processors consume events of one type, say UI Events and produce Events of another type, with the ability to perform actions in some Monad. These are used to process the UI Events that a Component emits into a form that that component's model can use to update its state.
In our simple todo mvc example, as we're using the same type for UI Events and Model Event, there's no processing and events are just passed straight through using the idProcessor.
-
Renderers
A Renderer is a function of type
DOMEvent a -> model -> HTMLwhere HTML is a virtual-dom representation of the UI.This is the top level
Rendererfrom todo mvc:todoView :: DOMEvent Action -> ToDo -> HTML todoView chan todo@(ToDo itemList _txtEntry currentFilter) = with div (classes .= ["body"]) [ titleRender, itemsRender, renderFilters chan todo] where titleRender = with h1 (classes .= ["title"]) ["todos"] itemsRender = with ul (classes .= ["items"]) (newItem chan todo : (P.map (renderItem chan) $ zip [0..] filteredItems)) filteredItems = filterItems currentFilter itemListIn this example
renderFilters,newItem, andrenderItemare allRenderersthat each render a sub part of the UIOne other point of interest is how
DOMEventswork if we take the example of onInput:onInput :: MonadState HTML m => DOMEvent String -> m ()In our
newItemRenderernewItem :: DOMEvent Action -> ToDo -> HTML newItem chan todo = with li (classes .= ["newItem"]) [ into form [ with input (do attrs . at "placeholder" ?= "Create a new task" props . at "value" ?= value onInput $ contramap SetEditText chan) [] , with (btn click "Create") (attrs . at "hidden" ?= "true") ["Create"] ] ] where value = (todo ^. editText.to toJSString) click = (const $ (channel chan) $ NewItem (todo ^. editText))we only have a
DOMEvent Actionavailable to accept UI Events, whereas onInput takes aDOMEvent Stringso we need to adapt theDOMEventpassed tonewItemto be one that takes aStringfor passing toonInput.DOMEventhappens to be an instance of theContravariantclass. You can thing of thecontramapfunction being like anfmap, but applying its function to the input of something rather than the content.f :: String -> Action f = SetEditText -- We have a DOMEvent Action -- We want a DOMEvent String -- fmap :: (a -> b) -> f a -> f b contramap :: (a -> b) -> f b -> f a contramap :: (String -> Action) -> DOMEvent Action -> DOMEvent String -
Components
A component packages up the three things that you provide into something that the framework can run, with an extra environment in
ReaderTthat the processor can use within its actions to route events that have an external effect (for example REST requests or socket.io calls)modelComp :: Component () Action ToDo Action modelComp = Component process todoView idProcessor main :: IO () main = void $ runComponent initialToDo () modelComp
http://todomvc.com/ The canonnical TODO MVC example demonstrates the basic moving parts of oHm
The socket.io example is a bit more involved and adds some new concepts illustrating nesting components by adapting the types of processors