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
value
property for each option tag and we need to be able to generate matching values for the currently selected record. - The argument to the
onChange
callback function will be anEvent
object. We need to look up the selected record separately. - Extra logic needs to be implemented each time a
select
is needed. In this example we render a disabledplaceholder
option 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})
:placeholder
is optional and used if no record is selected.:selected
is the selected record ornil
if none is selected.:data
is a sequence of records.:label-fn
is a function which takes a record and returns a string to be used as the option label.:key-fn
is a function which should return a unique string for each record.:on-select
is a callback function where the function argument is the selected record as opposed to anEvent
object.
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.