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 an Event 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 disabled placeholder 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 or nil 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 an Event 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.