Reusable Om components, part 1
November 29, 2014
This post is (hopefully) the first in a series on posts on how to create reusable components in Om. I will describe a simple declarative select component and the rationale behind wrapping such a trivial html element in an Om component. It assumes basic familiarity with Clojurescript and Om.
The running example will be rendering an input where the data source is a set of users, in this case the following set of distinguished computer scientists:
(def users
[{:first-name "Donald" :last-name "Knuth" :email "donald-knuth@example.com"}
{:first-name "Leslie" :last-name "Lamport" :email "leslie-lamport@example.com"}
{:first-name "Edsger" :last-name "Dijkstra" :email "edsger-dijskstra@example.com"}
{:first-name "John" :last-name "McCarthy" :email "john-mccarthy@example.com"}
{:first-name "Alan" :last-name "Kay" :email "alan-kay@example.com"}])
Imagine we're inside a component and wish to render a standard html select element populated with the above data. It would probably look something like this in pure Om:
(apply dom/select #js {:onChange on-user-selected
:value (or selected-user-id "__placeholder")}
(when-not selected
(dom/option #js {:disabled true :value "__placeholder"}
"Select a user"))
(map (fn [{:keys [first-name last-name email]}]
(dom/option #js {:value email}
last-name ", " first-name))
users))
This isn't too bad, but let's focus on a few possible problems:
- We need to build each option manually.
- We need to choose (or construct) a suitable unique string to use as the
valueproperty for each option tag and we need to be able to generate matching values for the currently selected record. - The argument to the
onChangecallback function will be anEventobject. We need to look up the selected record separately. - Extra logic needs to be implemented each time a
selectis needed. In this example we render a disabledplaceholderoption if no user is selected.
I consider all these points to be incidental complexity. Of course, it's possible to cut away some of the Om boilerplate by using either sablono or om-tools. With sablono the equivalent code would be
[:select {:on-change on-user-selected
:value (or selected-user-id "__placeholder")}
(when-not selected
[:option {:disabled true :value "__placeholder"} "Select a user"])
(map (fn [{:keys [first-name last-name email]}]
[:option {:value email} last-name ", " first-name])
users)]
As you hopefully realize, the above code snippet is a bit shorter and perhaps easier to read but the issues raised above are still the same. You can make the complexity more concise, but you can't remove it with these tools alone.
An alternative select component
Next we will build a component without the incidental complexities mentioned above. The component, once implemented, will be used as follows:
(om/build select
{:placeholder "Select a user"
:selected selected-user
:data users
:label-fn #(str (:last-name %) ", " (:first-name %))
:key-fn :email
:on-select on-user-selected})
:placeholderis optional and used if no record is selected.:selectedis the selected record ornilif none is selected.:datais a sequence of records.:label-fnis a function which takes a record and returns a string to be used as the option label.:key-fnis a function which should return a unique string for each record.:on-selectis a callback function where the function argument is the selected record as opposed to anEventobject.
This component is easy to read and write, declerative and reusable. The implementation of the select component is not too complex either:
(defn select [{:keys [placeholder data selected label-fn key-fn on-select]} owner]
(reify
om/IRender
(render [this]
(apply dom/select
#js {:onChange (fn [evt]
(let [key (-> evt .-target .-value)
sel (some (fn [record]
(if (= key (key-fn record))
record))
data)]
(on-select sel)))
:value (if selected
(key-fn selected)
"__placeholder")}
(when (and placeholder (not selected))
(dom/option #js {:disabled true :value "__placeholder"} placeholder))
(map (fn [record]
(dom/option #js {:value (key-fn record)} (label-fn record)))
data)))))
When working with Om I strive to create components like this. I've found that these stateless and declarative components are the most reliable and reusable. In addition, they work very well with how Om takes advantage of immutable data structures in order to avoid unnecessary rendering.
If you want to build and run these examples on your own, complete examples are available in the nil/recur github repo.