I recently asked the following on the clojurescript mailing list:
Does anyone find themselves inexorably morphing Om into Backbone?
It feels like a horror movie, honestly.
(defn radial-menu [{:keys [menu-choices pos] :as data} owner]
(reify
om/component
(rendering code)Oh, wait a minute, the choices change (recursive menu) and the position feels like it should live in the component rather than in the application state. Also it needs a channel for when it’s supposed to change position: Also, I need a reference to the main application state:
(defn radial-menu [app owner]
(reify
om/IInitState
(init-state [_]
{:phase :closed
:change-position (chan)})om/IWillMount
(will-mount [_]
(go (loop []
(let [new-pos (<! (om/get-state owner :change-position))]
(om/set-state! :pos new-pos)))))om/IRenderState
(render-state [this {:keys [pos menu-choices]}]
………Hmm, that felt like a lot of boilerplate just to change the position. I should write a macro for values local to the component that exposes a channel and changes the value when the channel receives a message, something like
(defcomponent radial-menu
{:vals [:position]
:render-body
(bla bla bla)}Wait, that looks like an OBJECT.
Can someone talk me out of this, or is there no way out?
So, the first thing you have to understand is that this question is terrible. Like, really terrible. It misses the entire point of Om.
The second thing you have to understand is that it’s not terrible at all. I wasn’t high, drunk, tired, or even hungry when I posted the question. I suspect others would be prone to the same question if they knew Om the library pretty well (I do, after all) but not Om the context (I did, but I had forgotten it).
One of the ideas behind Om is a functional user interface. Clojure programs tend to have tightly-controlled state in one corner of the program, and then the rest of the program is made up of pure functions that operate on the data.
This is explicitly opposite to object-oriented programming(like, say, Backbone), which tends to spread state out among a million different objects.
Just knowing that part should give us an idea about my first few concerns:
(defn radial-menu [{:keys [menu-choices pos] :as data} owner]
(reify
om/component
(rendering code)Oh, wait a minute, the choices change (recursive menu) and the position feels like it should live in the component rather than in the application state. Also it needs a channel for when it’s supposed to change position: Also, I need a reference to the main application state:
Here the state is carried to the component in the cursor (first argument) and that’s a good thing. The position should not live in the component-local state—it should live in application state. And while channels are good, that’s not how the position is updated. Since the position lives in the application global state, Om will re-render the radial-menu component if the position changes.
Okay, so what updates position in the global state? Is there a function attached to some element that activates on click?
No! We are trying to keep UI presentation and logic separate. So we introduce a layer of indirection: channels.
So we might have:
(defn radial-menu [app owner options]
(apply dom/g #js {:transform (stuff depending on (:position app))
(om/build-all radial-button (app :buttons)) options)
(defn radial-button [button owner options]
(dom/g #js {:transform stuff }
(dom/rect #js {:fill “black” …. :onClick ??????????????????????))
The radial-button, when clicked, has a very simple callback:
(put! (get-in options [:channels :radial-click] @button))
Note the presence of :channels in the third parameter we’ve been passing down. Generally this is the only way you’ll be able to communicate “up” the tree (you can also pass channels into the component state through :init-state in the third parameter. I am unclear on when this would be useful, because it seems like you can get the same thing but stateless just by taking it as a param)
And that’s really all you put in your UI.
“Wait a sec,” you might say. “You didn’t do anything with that click, you just put it on a channel.”
Yes! And here is where the (semi-surprising) meat of Om comes in. If you go through the tutorials and think fun thoughts about instrumentation and visualization you may get the idea that the process of writing an Om app is:
1.Write a component.
2. Repeat until done.
Au contraire mon frere, Om is meant to cover just the third element—interface representation—of David Nolen’s suggested trichotomy of event stream processing, event stream coordination, and interface representation. This means there are sizeable portions left to write! It makes sense—the logic may not be complected up with the UI components, but it has to live somewhere. That somewhere is the streams.
It’s OK to Cross Them
Let’s take a look at the old-at-25-days reference Om app, Zenbox’s Omchaya.
Hmm. Let’s take a closer look. We’re mostly looking at structure here:
I just wanted to make it unbearably clear, because this is the mistake I was making thinking about Backbone. This isn’t your (I was going to say grandfather, but things move so fast I’ll use “older brother”)’s MVC. It’s not MVC.
But you do need some sort of structure. Om may be all about components, but the reason it exists is because the author wanted it to support stream-based architecture. And that architecture is where the real action is.
To start, let’s take a look at Omchaya’s app.cljs:
Look at that block—that’s not logic so much as data. Remember the channel our button was writing to? If all your input is done by channels…well, you’re gonna need a lot of channels. Don’t be afraid of them.
So we have Om writing from the application state to the UI, and the UI writing to channels in the world’s smallest callbacks…we need something to write from the channels to the application state. And here’s where it all comes together in functiony goodness. Data in from the channels, transactions out to Om holding the application state? Why, the only thing we need in between are…functions. Functions to route events, functions to act based on events…
To summarize, data flows:
- from your state to your UI (via Om)
- from your UI to your channels (via one-liner callbacks)
- from your channels to the right functions (via stream combinators and routing functions)
- from your functions to your state (via om/transact! and update!)
I hope I’ve atoned for my braindead question now, and perhaps someone will “get it” as a result of this post. Well, I did.