Skip to content
John Arthur edited this page May 30, 2018 · 91 revisions

Note: this tutorial is for Om, not Om Next

This tutorial assumes familiarity with Clojure or ClojureScript. If you are not familiar with them, we recommend going through the ClojureScript tutorial for Light Table first. If you are entirely new to Om, you may also benefit from a quick look through the Conceptual overview.

We will use Figwheel to easily get an interactive environment for ClojureScript and Om. Figwheel does two things:

  • It automatically compiles and reloads your ClojureScript and CSS in the browser as soon as you save the file, no need to refresh.
  • It connects the browser to a REPL so you can try expressions and manipulate your running app.

To get started, install Leiningen. Then navigate in a terminal to the directory where you would like the tutorial and run the following command:

lein new figwheel om-tut -- --om

This will create a project including Om and Figwheel in a folder called om-tut. cd into it and run the following command:

lein figwheel

It will start auto building the ClojureScript project. The first build will take a few seconds. Once the build has succeeded open localhost:3449 in your favorite browser (we recommend Google Chrome as it has excellent support for source maps). You should see an h1 tag with the text content Hello World! in it.

Open src/om_tut/core.cljs in your preferred editor. Change the :text value of app-state to be something else other than Hello World!. Save the file. Refresh your browser and you should see the new contents. The reason we need to refresh the browser is because app-state is defined with defonce. This is meant to prevent each reload from resetting the state.

That's pretty boring isn't it? Let's do some live coding instead. Arrange your windows so that you can see both the Chrome window and your source code at the same time.

Now change dom/h1 to dom/p and watch the browser window. It should change without the need for reloading. This changed the behavior of the code without affecting app-state. To edit app-state you can either refresh the browser to reset it, or manipulate it directly from the browser-connected-REPL. In the same terminal window where you entered lein figwheel you should see a REPL. If you don't, try refreshing your browser window. An easy way to try the REPL is:

cljs.user> (js/alert "Am I connected?")

You should see the alert in your browser. After you click "Ok" in the browser, the expression should return nil in the REPL. If you see exceptions and error messages, please troubleshoot Figwheel's Browser REPL before proceeding. If everything is working move from the generic cljs.user namespace to om-tut.core (where all our code lives) and then change the state:

cljs.user=> (in-ns 'om-tut.core)
nil
om-tut.core=> (swap! app-state assoc :text "Do it live!")

Om basics

In Om the application state is held in an atom, the one reference type built into ClojureScript. If you change the value of the atom via swap! or reset! this will always trigger a re-render of any Om roots attached to it (we'll explain this in a second). You can think of this atom as the database of your client side application. Everything in the atom should be an associative data structure - either a ClojureScript map or indexed sequential data structure such as a vector (but not a set). This means you should never put lists or lazy sequences into the application state. It's particularly easy to forget this when updating indexed sequences in the application state.

om.core/root

om.core/root (which is aliased to om/root here), establishes an Om rendering loop on a specific element in the DOM. The om.root expression in the tutorial at this point looks like this:

(om/root
  (fn [data owner]
    (reify
      om/IRender
      (render [_]
        (dom/p nil (:text data)))))
  app-state
  {:target (. js/document (getElementById "app"))})

om.core/root is idempotent; that is, it's safe to evaluate it multiple times (that is why we don't need defonce). It takes three arguments. The first argument is a function that takes the application state data and the backing React component, here called owner. This function must return an Om component - i.e. a model of the om/IRender interface, like om.core/component macro generates. The second argument is the application state atom. The third argument is a map; it must contain a :target DOM node key value pair. It also takes other interesting options which will be covered later.

There can be multiple roots. Edit resources/public/index.html, replace <div id="app"><h2>Figwheel..</h2><p>Checkout..</p></div> with the following:

<div id="app0"></div>
<div id="app1"></div>

You need to refresh the browser after changing resources/public/index.html since Figwheel doesn't handle HTML reloading. And edit src/om_tut/core.cljs replacing the om/root expression with the following:

(om/root
  (fn [data owner]
    (om/component (dom/h2 nil (:text data))))
  app-state
  {:target (. js/document (getElementById "app0"))})

Refresh your browser. You should see one h2 tag on the page. Copy and paste the om/root expression and edit the second one to look like the following:

(om/root
  (fn [data owner]
    (om/component (dom/h2 nil (:text data))))
  app-state
  {:target (. js/document (getElementById "app1"))}) ;; <-- "app0" to "app1"

You should see the second h2 tag magically appear after saving.

Evaluate this in the REPL:

om-tut.core=> (swap! app-state assoc :text "Multiple roots!")

You should see both h2 tags update on the fly. Multiple roots are fully supported and synchronized to render on the same requestAnimationFrame.

Before proceeding remove the <div id="app1"></div> from resources/public/index.html and remove the second om/root expression. Save and refresh the browser.

Rendering a list of things

Change the app-state expression to the following and refresh the browser:

(defonce app-state (atom {:list ["Lion" "Zebra" "Buffalo" "Antelope"]}))

If we didn't use defonce the state would be restarted each time we change our code because of Figwheel's code reloading. If we wanted to try new code with the current app-state it would be impossible.

Change the om/root expression to the following and save. Don't bother refreshing, John McCarthy would be pissed! You should see a list of animals now.

(om/root
  (fn [data owner]
    (om/component
      (apply dom/ul nil
        (map (fn [text] (dom/li nil text)) (:list data)))))
  app-state
  {:target (. js/document (getElementById "app0"))})

You might have noticed that the first argument to dom/ul and dom/li is nil. This argument is how you set DOM attributes. Change the om/root expression to the following and save it:

(om/root
  (fn [data owner]
    (om/component
      (apply dom/ul #js {:className "animals"}
        (map (fn [text] (dom/li nil text)) (:list data)))))
  app-state
  {:target (. js/document (getElementById "app0"))})

If you right click on the list in Google Chrome and select Inspect Element, you should see that the ul tag in the DOM now has its CSS class attribute set to "animals".

#js {...} and #js [...] are reader literals. ClojureScript supports data literals for JavaScript via #js.

#js {...} is for JavaScript objects:

#js {:foo "bar"}  ;; is equivalent to
#js {"foo" "bar"}

#js [...] is for JavaScript arrays:

#js [1 2 3]

The #js reader literal support is shallow: if you nest an array or object inside another, it'll be treated as the ClojureScript type, not a JavaScript type. For instance:

#js {:foo [1 2 3]} ;; a JS object with a persistent vector in it

Life without a templating language

In Om, you have the full power of the ClojureScript language when building your user interface. But Om also leaves the door open for alternate syntaxes to describe the DOM if that's your cup of tea.

Let's edit our code so we get zebra striping on the list. Let's add a helper function stripe before the om/root expression:

(defn stripe [text bgc]
  (let [st #js {:backgroundColor bgc}]
    (dom/li #js {:style st} text)))

Then change the om/root expression to the following and save:

(om/root
  (fn [data owner]
    (om/component
      (apply dom/ul #js {:className "animals"}
        (map stripe (:list data) (cycle ["#ff0" "#fff"])))))
  app-state
  {:target (. js/document (getElementById "app0"))})

As we can see ClojureScript offers powerful functional tools that can produce complex HTML without the use of templates.

Your first Om component

Change <div id="app0"></div> to <div id="contacts"></div>, remove stripe from src/om_tut/core.cljs and save.

Change the om/root expression to the following, don't save it yet since we haven't defined contacts-view.

(om/root contacts-view app-state
  {:target (. js/document (getElementById "contacts"))})

Let's edit app-state so it looks like this:

(defonce app-state
  (atom
    {:contacts
     [{:first "Ben" :last "Bitdiddle" :email "benb@mit.edu"}
      {:first "Alyssa" :middle-initial "P" :last "Hacker" :email "aphacker@mit.edu"}
      {:first "Eva" :middle "Lu" :last "Ator" :email "eval@mit.edu"}
      {:first "Louis" :last "Reasoner" :email "prolog@mit.edu"}
      {:first "Cy" :middle-initial "D" :last "Effect" :email "bugs@mit.edu"}
      {:first "Lem" :middle-initial "E" :last "Tweakit" :email "morebugs@mit.edu"}]}))

After app-state let's add the following code:

(defn contacts-view [data owner]
  (reify
    om/IRender
    (render [this]
      (dom/div nil
        (dom/h2 nil "Contact list")
        (apply dom/ul nil
          (om/build-all contact-view (:contacts data)))))))

To build an Om component we must use om.core/build for a single component and om.core/build-all to build many components. In this case we want to display a contact list so we use a vector of contacts as the cursor. contacts-view returns a div with a h2 and ul tag in it. We want to render several li elements so we call apply on dom/ul. If you wanted to render a single component, the cursor would probably not be a vector and you would om.core/build the component without applying it to a root node.

Let's write contact-view now and add it after contacts-view.

(defn contact-view [contact owner]
  (reify
    om/IRender
    (render [this]
      (dom/li nil (display-name contact)))))

Now save the file and reload the browser. You'll see no changes and some errors in the the browser console or in the same screen (look for the yellow message in the bottom). Figwheel refuses to load code that has warnings. Go to the console:

WARNING: Use of undeclared Var om-tut.core/contact-view at line 24 src/om_tut/core.cljs
WARNING: Use of undeclared Var om-tut.core/display-name at line 30 src/om_tut/core.cljs

It warns that contact-view is undefined, because it's defined after contacts-view.

So let's move it before and save it again to check that the warning is gone and we only have the warning about the actually missing display-name function.

So now for display-name. Let's add it before contact-view.

(defn display-name [{:keys [first last] :as contact}]
  (str last ", " first (middle-name contact)))

Here we get a taste of map destructuring. Pretty handy. Finally let's write the middle-name helper, it should come before display-name in your file.

(defn middle-name [{:keys [middle middle-initial]}]
  (cond
    middle (str " " middle)
    middle-initial (str " " middle-initial ".")))

Again some map destructuring.

After you save you should see the list of contacts.

Enhancing your first Om component

Let's try deleting contacts. Change contact-view to the following:

(defn contact-view [contact owner]
  (reify
    om/IRender
    (render [this]
      (dom/li nil
        (dom/span nil (display-name contact))
        (dom/button nil "Delete")))))

Save it. You should see delete buttons now, however clicking on them won't do anything.

Contacts don't need to be able to delete themselves, however they should be able to communicate to some entity that does have that power.

Intercomponent communication

For communication between components we will use core.async channels. Change your namespace form to the following:

(ns ^:figwheel-always om-tut.core
  (:require-macros [cljs.core.async.macros :refer [go]])
  (:require [om.core :as om :include-macros true]
            [om.dom :as dom :include-macros true]
            [cljs.core.async :refer [put! chan <!]]))

Save your file and refresh the browser. (Note: For this to work, there is already the dependency [org.clojure/core.async "x.x.x"] in your project.clj. When adding this manually to future projects you will need to stop/restart the lein figwheel process). Change contact-view to the following:

(defn contact-view [contact owner]
  (reify
    om/IRenderState
    (render-state [this {:keys [delete]}]
      (dom/li nil
        (dom/span nil (display-name contact))
        (dom/button #js {:onClick (fn [e] (put! delete @contact))} "Delete")))))

We've changed om/IRender to om/IRenderState. This is because we will receive the delete notification channel as part of our component state. We've also added a onClick handler to the button which writes the contact onto the channel. This is actually a bug as we will soon see.

Change contacts-view to the following. It's a big change, don't worry we'll walk through all of it.

(defn contacts-view [data owner]
  (reify
    om/IInitState
    (init-state [_]
      {:delete (chan)})
    om/IWillMount
    (will-mount [_]
      (let [delete (om/get-state owner :delete)]
        (go (loop []
             (let [contact (<! delete)]
               (om/transact! data :contacts
                 (fn [xs] (vec (remove #(= contact %) xs))))
               (recur))))))
    om/IRenderState
    (render-state [this {:keys [delete]}]
      (dom/div nil
        (dom/h2 nil "Contact list")
        (apply dom/ul nil
          (om/build-all contact-view (:contacts data)
            {:init-state {:delete delete}}))))))

Reminder: Notice that we're using vec to transform the result of remove (a lazy sequence) back into a vector, consistent with our aforementioned rule that state should only consist of associative data structures like maps and vectors.

First we set the initial state by implementing om/IInitState. We allocate a core.async channel. It's extremely important we don't do this in a let binding around reify. This is a common mistake. Remember the contacts-view function will potentially be invoked many, many times. We also change om/IRender to om/IRenderState as we want to get the delete channel so we can pass it down.

We then implement om/IWillMount so that we can establish a go loop that will listen for events from the children contact views. If we get a delete event, we remove the associated contact from the application state with om.core/transact!. You should now be able to delete people from the contact list.

Adding Contacts

Let's modify our application so we can add new contacts. Change the top namespace form to the following, restart lein figwheel, and refresh your browser:

(ns ^:figwheel-always om-tut.core
  (:require-macros [cljs.core.async.macros :refer [go]])
  (:require [om.core :as om :include-macros true]
            [om.dom :as dom :include-macros true]
            [cljs.core.async :refer [put! chan <!]]
            [clojure.string :as string]))

Let's add a new function called parse-contact and save it.

(defn parse-contact [contact-str]
  (let [[first middle last :as parts] (string/split contact-str #"\s+")
        [first last middle] (if (nil? last) [first middle] [first last middle])
        middle (when middle (string/replace middle "." ""))
        c (if middle (count middle) 0)]
    (when (>= (count parts) 2)
      (cond-> {:first first :last last}
        (== c 1) (assoc :middle-initial middle)
        (>= c 2) (assoc :middle middle)))))

There are of course many ways to write parse-contact and this is not particularly the best way, however it illustrates many common idioms. If you're not experienced with Clojure or ClojureScript it's worth taking the time to understand it before proceeding.

Try it on some input!

(in-ns 'om-tut.core)
(parse-contact "Gerald J. Sussman")

If Figwheel throws a ReferenceError with the message clojure is not defined or string is not defined, shut down the REPL. Then enter the command:

lein clean

... restarting Figwheel and refreshing the browser will clear the problem.

Once you've seen that it basically works, let's write add-contact. It should look like the following:

(defn add-contact [data owner]
  (let [new-contact (-> (om/get-node owner "new-contact")
                        .-value
                        parse-contact)]
    (when new-contact
      (om/transact! data :contacts #(conj % new-contact)))))

We need to use om.core/get-node so that we can extract the value from the text field. We'll see how we set this up, your contacts view should look like the following:

(defn contacts-view [data owner]
  (reify
    om/IInitState
    (init-state [_]
      {:delete (chan)})
    om/IWillMount
    (will-mount [_]
      (let [delete (om/get-state owner :delete)]
        (go (loop []
              (let [contact (<! delete)]
                (om/transact! data :contacts
                  (fn [xs] (vec (remove #(= contact %) xs))))
                (recur))))))
    om/IRenderState
    (render-state [this state]
      (dom/div nil
        (dom/h2 nil "Contact list")
        (apply dom/ul nil
          (om/build-all contact-view (:contacts data)
            {:init-state state}))
        (dom/div nil
          (dom/input #js {:type "text" :ref "new-contact"})
          (dom/button #js {:onClick #(add-contact data owner)} "Add contact"))))))

Notice that the input field specified :ref, this is a feature of React for the few cases where you need direct access to a DOM node.

Save the file. You should now be able to add contacts as long as you provide at least a first and last name.

This is a lot of information. As a challenge I recommend trying to clear the text field when a real contact has been added. This is harder than it looks so don't get discouraged. If you spend more than 15, 20 minutes on it feel free to proceed to the next section and we'll show how to do it.

Dealing with text input fields

React's declarative model, and thus Om's, makes dealing with input both a little more challenging, but also more flexible. The "easiest" way to clear the text field would be by changing add-contact to the following:

(defn add-contact [data owner]
  (let [input (om/get-node owner "new-contact")
        new-contact (-> input .-value parse-contact)]
    (when new-contact
      (om/transact! data :contacts #(conj % new-contact))
      (set! (.-value input) ""))))

This works great, but any time you find yourself leaning heavily on refs it's probably worth stepping back and considering whether the same thing could be accomplished in a more declarative manner.

Since contacts-view "owns" the text field we should consider making its value a part of contacts-view's local state. Let's change contacts-view to the following and save it:

(defn contacts-view [data owner]
  (reify
    om/IInitState
    (init-state [_]
      {:delete (chan)
       :text ""})
    om/IWillMount
    (will-mount [_]
      (let [delete (om/get-state owner :delete)]
        (go (loop []
              (let [contact (<! delete)]
                (om/transact! data :contacts
                  (fn [xs] (vec (remove #(= contact %) xs))))
                (recur))))))
    om/IRenderState
    (render-state [this state]
      (dom/div nil
        (dom/h2 nil "Contact list")
        (apply dom/ul nil
          (om/build-all contact-view (:contacts data)
            {:init-state state}))
        (dom/div nil
          (dom/input #js {:type "text" :ref "new-contact" :value (:text state)})
          (dom/button #js {:onClick #(add-contact data owner)} "Add contact"))))))

Try typing in the text field.

Yikes! You can no longer enter anything. Let's take a moment to consider what's going on.

We've added a new piece of state to contacts-view. Regardless of what the user may type we are now setting the value of the input field to the value of the :text state property. We need to keep this in sync with the user input. Let's change contacts-view again by adding an event listener to watch when the input field changes:

(defn contacts-view [data owner]
  (reify
    om/IInitState
    (init-state [_]
      {:delete (chan)
       :text ""})
    om/IWillMount
    (will-mount [_]
      (let [delete (om/get-state owner :delete)]
        (go (loop []
              (let [contact (<! delete)]
                (om/transact! data :contacts
                  (fn [xs] (vec (remove #(= contact %) xs))))
                (recur))))))
    om/IRenderState
    (render-state [this state]
      (dom/div nil
        (dom/h2 nil "Contact list")
        (apply dom/ul nil
          (om/build-all contact-view (:contacts data)
            {:init-state state}))
        (dom/div nil
          (dom/input
            #js {:type "text" :ref "new-contact" :value (:text state)
                 :onChange #(handle-change % owner state)})
          (dom/button #js {:onClick #(add-contact data owner)} "Add contact"))))))

Before saving that, let's add handle-change before contacts-view:

(defn handle-change [e owner {:keys [text]}]
  (om/set-state! owner :text (.. e -target -value)))

Now save all the changes. You should now be able to type in the text field again.

Let's finally add the piece of code that clears the text field. As you see it looks like our first "easy" attempt, except that we're no longer directly manipulating a ref but we're changing the state of the app:

(defn add-contact [data owner]
  (let [new-contact (-> (om/get-node owner "new-contact")
                        .-value
                        parse-contact)]
    (when new-contact
      (om/transact! data :contacts #(conj % new-contact))
      (om/set-state! owner :text ""))))

That seemed like a lot of work for little gain ... except we just saw we have really fine grained control over user input entry. For example a name can't have a number in it, let's prevent that now by modifying handle-change:

(defn handle-change [e owner {:keys [text]}]
  (let [value (.. e -target -value)]
    (if-not (re-find #"[0-9]" value)
      (om/set-state! owner :text value)
      (om/set-state! owner :text text))))

Now save. You can type names however no change will occur if you attempt to enter a number. Pretty slick.

Note: If you are familiar with React you'll notice that this is a little bit clunkier than in React, here we have to make sure to set the state of the text field even if we don't want it to change. This is a side effect of React's internals clashing a bit with Om's optimization of always rendering on requestAnimationFrame.

Higher Order Components

The most powerful components are those whose sub-components can be parameterized. To focus on this concept let's leave aside user input and other complications for now. As a challenge you should try to re-add these facilities yourself after you've worked through this section.

Let's start fresh. Your resources/public/index.html should look like the following:

<!DOCTYPE html>
<html>
  <head>
    <link href="css/style.css" rel="stylesheet" type="text/css">
  </head>
  <body>
    <div id="registry"></div>
    <script src="js/compiled/om_tut.js" type="text/javascript"></script>
  </body>
</html>

Your source file should look like the following:

(ns ^:figwheel-always om-tut.core
  (:require [om.core :as om :include-macros true]
            [om.dom :as dom :include-macros true]
            [clojure.string :as string]))

(enable-console-print!)

(def app-state
  (atom
    {:people
     [{:type :student :first "Ben" :last "Bitdiddle" :email "benb@mit.edu"}
      {:type :student :first "Alyssa" :middle-initial "P" :last "Hacker"
       :email "aphacker@mit.edu"}
      {:type :professor :first "Gerald" :middle "Jay" :last "Sussman"
       :email "metacirc@mit.edu" :classes [:6001 :6946]}
      {:type :student :first "Eva" :middle "Lu" :last "Ator" :email "eval@mit.edu"}
      {:type :student :first "Louis" :last "Reasoner" :email "prolog@mit.edu"}
      {:type :professor :first "Hal" :last "Abelson" :email "evalapply@mit.edu"
       :classes [:6001]}]
     :classes
     {:6001 "The Structure and Interpretation of Computer Programs"
      :6946 "The Structure and Interpretation of Classical Mechanics"
      :1806 "Linear Algebra"}}))

(defn middle-name [{:keys [middle middle-initial]}]
  (cond
    middle (str " " middle)
    middle-initial (str " " middle-initial ".")))

(defn display-name [{:keys [first last] :as contact}]
  (str last ", " first (middle-name contact)))

(defn registry-view [data owner]
  (reify
    om/IRenderState
    (render-state [_ state]
      (dom/div nil
        (dom/h2 nil "Registry")))))

(om/root registry-view app-state
  {:target (. js/document (getElementById "registry"))})

(defn on-js-reload []
  ;; optionally touch your app-state to force rerendering depending on
  ;; your application
  ;; (swap! app-state update-in [:__figwheel_counter] inc)
)

Now what we want is for registry-view to render different views for different types of people without hardcoding. Here we'll see how multimethods in ClojureScript really shine.

After display-name let's write the following:

(defmulti entry-view (fn [person _] (:type person)))

(defmethod entry-view :student
  [person owner] (student-view person owner))

(defmethod entry-view :professor
  [person owner] (professor-view person owner))

Don't save these yet as we haven't written student-view or professor-view. Take a moment to let the idea sink, we're adding a level of indirection so that entry-view can delegate to any other view as long as we supplied a defmethod for it!

Let's write the underlying views now before entry-view:

(defn student-view [student owner]
  (reify
    om/IRender
    (render [_]
      (dom/li nil (display-name student)))))

(defn professor-view [professor owner]
  (reify
    om/IRender
    (render [_]
      (dom/li nil
        (dom/div nil (display-name professor))
        (dom/label nil "Classes")
        (apply dom/ul nil
          (map #(dom/li nil %) (:classes professor)))))))

Finally let's fix up registry-view. It's succinct and we've kept it clean of conditionals.

(defn registry-view [data owner]
  (reify
    om/IRender
    (render [_]
      (dom/div #js {:id "registry"}
        (dom/h2 nil "Registry")
        (apply dom/ul nil
          (om/build-all entry-view (people data)))))))

The only missing bit now is the people function. We want to make sure to render professors with their list of actual class titles. Before registry-view write the following:

(defn people [data]
  (->> data
    :people
    (mapv (fn [x]
            (if (:classes x)
              (update-in x [:classes]
                (fn [cs] (mapv (:classes data) cs)))
              x)))))

Save everything and you should see the results.

Hopefully the view composition and extensibility offered by Om puts a gleam in your eye. There's more than enough here to keep you occupied for hours if you wish to take it further yourself.

Otherwise in the next section we'll show you how we can easily modify each professor's class list and make those changes visible in multiple locations on the screen.

Interactivity & Higher Order Components

Let's change resources/public/index.html to the following:

<!DOCTYPE html>
<html>
  <head>
    <link href="css/style.css" rel="stylesheet" type="text/css">
  </head>
  <body>
    <div id="registry"></div>
    <div id="classes"></div>
    <script src="js/compiled/om_tut.js" type="text/javascript"></script>
  </body>
</html>

and resources/public/css/style.css to:

ul li input {
    width: 400px;
}
ul li button {
    margin-left: 10px;
}

Let's add classes-view after registry-view:

(defn classes-view [data owner]
  (reify
    om/IRender
    (render [_]
      (dom/div #js {:id "classes"}
        (dom/h2 nil "Classes")
        (apply dom/ul nil
          (map #(dom/li nil %) (vals (:classes data))))))))

Let's use it by adding a new om/root expression after the existing one:

(om/root classes-view app-state
  {:target (. js/document (getElementById "classes"))})

Save all the files, you should see the new bits of UI.

Let's make class names editable inline. To accomplish this we want to make a reusable component that we can plug in wherever we like. This will be the most complicated component we have seen so far.

When the user edits a class name we should hide the original text and present an input field. So we need a little helper function to do this. After app-state add display:

(defn display [show]
  (if show
    #js {}
    #js {:display "none"}))

Every time a user presses a key while editing a class name we want to update the application state:

(defn handle-change [e text owner]
  (om/transact! text (fn [_] (.. e -target -value))))

When the input loses focus we want to exit editing mode:

(defn commit-change [text owner]
  (om/set-state! owner :editing false))

These are all helpers for our soon to be written editable component. editable takes a JavaScript string and presents it while also making it editable. For this to work, the JavaScript string needs to support the Om cursor interface. We don't need to implement this ourselves but we do need to make sure that JavaScript strings implement ICloneable so that Om can do the hard work for us (Note the following is for demonstration purposes only, it is not recommended in most real applications. Please refer to the Intermediate Tutorial for a better approach that does not require extending JavaScript native strings to ICloneable*).

Be sure to put these extend-type forms before the om/root forms. Putting them near the top of the file will do nicely.

(extend-type string
  ICloneable
  (-clone [s] (js/String. s)))

Sadly this is not enough because JavaScript String objects and JavaScript primitive strings are not the same thing:

(extend-type js/String
  ICloneable
  (-clone [s] (js/String. s))
  om/IValue
  (-value [s] (str s)))

We'll explain IValue in a moment. The ClojureScript compiler will emit a warning. Normally you don't want to ignore it but in this case we'll make an exception in order to keep the editable component as simple as possible. Since Figwheel doesn't reload code with warnings by default add this piece of configuration to Figwheel on project.clj:

              :figwheel { :load-warninged-code true  ;; <- Add this
                          :on-jsload "om-tut-v1.core/on-js-reload" }

Run lein clean (just in case) and restart lein figwheel.

This is the editable component; this might look like a lot but take a moment to read it and you'll see that it's quite simple.

(defn editable [text owner]
  (reify
    om/IInitState
    (init-state [_]
      {:editing false})
    om/IRenderState
    (render-state [_ {:keys [editing]}]
      (dom/li nil
        (dom/span #js {:style (display (not editing))} (om/value text))
        (dom/input
          #js {:style (display editing)
               :value (om/value text)
               :onChange #(handle-change % text owner)
               :onKeyDown #(when (= (.-key %) "Enter")
                              (commit-change text owner))
               :onBlur (fn [e] (commit-change text owner))})
        (dom/button
          #js {:style (display (not editing))
               :onClick #(om/set-state! owner :editing true)}
          "Edit")))))

We have to use om.core/value here because React doesn't know how to handle JavaScript String objects. This is also why we implement IValue above.

We also need to update professor-view since the class title might be a String object instead of a primitive string:

(defn professor-view [professor owner]
  (reify
    om/IRender
    (render [_]
      (dom/li nil
        (dom/div nil (display-name professor))
        (dom/label nil "Classes")
        (apply dom/ul nil
          (map #(dom/li nil (om/value %)) (:classes professor)))))))

Let's use editable in classes-view:

(defn classes-view [data owner]
  (reify
    om/IRender
    (render [_]
      (dom/div #js {:id "classes"}
        (dom/h2 nil "Classes")
        (apply dom/ul nil
          (om/build-all editable (vals (:classes data))))))))

That's it, save it. You should now be able to edit class titles in the classes-view. Notice that the class titles in registry-view stay perfectly in sync.

As a challenge, render the classes in professor-view with editable instead of just rendering strings. It's just a one line change. If you get it working notice how editing the class titles from deep within the registry-view "just works". This is the modularity that Om cursors provide.