Common mistakes to avoid when creating an Om component. Part 2.

It’s been a while since the last post. More mistakes have been made, lessons have been learned, so here’s a handful:

  1. Combining cursors

    Make sure you’re updating the cursor and not the map that combines them.
    Let’s say you want to create a component that has a view of bands and titles of films. You don’t want to pass whole of the app state, you just want to pass :albums and :titles. You combine them like this:

    (def app-state {:music {:artists []
                            :bands {:faved []}
                            :albums {:faved []}}
                    :films {:directors []
                            :titles {:faved []}}})
    
    (om/build select-favourites {:bands  (-> cursor :music :bands)
                                 :titles (-> cursor :films :titles)})
    
    

    Inside of you select-favourites component you might assume that cursor is an actual cursor. It’s not, it’s just a map containing cursors. You can’t do this:

    (defn select-favourites [cursor owner]
      (om/component
       ...
       (om/transact! cursor [:bands :faved] #(conj % some-band))))
    

    You can’t update the map. You need to get the cursor out of the map:

    (defn select-favourites [{:keys [bands titles]} owner]
      (om/component
       ...
       (om/transact! bands :faved #(conj % some-band))))
    
  2. Method signature and protocol implementation

    Sometimes you may forget the protocol implementation or you may provide something that doesn’t match the method signature. If you see no meaningful errors coming from the compiler, upgrade ClojureScript.

    (defn some-component [cursor owner]
      (reify
        om/IRenderState
        (render [_])))
    

    ClojureScript compiler warns you nicely about the mistake:

    WARNING: Bad method signature in protocol implementation, om/IRenderState does not declare method called render at line 159 src/cljs/pumpkin/core.cljs
    

    I remember seeing java.lang.IndexOutOfBoundsException which was caused by missing protocol implementation. Upgrading helped. Try to stay up to date with both Om and ClojureScript. You’ll see more meaningful errors and warnings.

  3. Don’t do computations in render

    If you need to group-by, or do other sorts of data transformations, it’s a bad idea to do it in render as it will slow it down. Try to transform your data as early as possible if the transformation is not caused by immediate user interaction. I usually do it in will-mount.

  4. Don’t create mult channel in render

    Let’s say some parent component creates a number of components that depend on size of your data and you want all children to receive messages broadcasted on a core.async channel. Create a mult of that channel in init-state. Then, when children components are built in render, create a copy of that mult channel using tap. If you create mult in render all sorts of weirdness will happen.

  5. Oh god, don’t use lazy sequences in cursors.

    This one should be obvious, but if you map, remove or filter something in your data and update the cursor with the result, you will end up with a lazy sequence. Remember about mapv, filterv or into [].

That’s all I remember. Hope it’s useful!

Draggable wrapper component with Om and core.async

I’ve been looking for a way to enable dragging of Om components, something similar to what Draggable does but much much simpler. I just want to drag component around the UI, no bells and whistles. I didn’t want to add this functionality to each component but just enable it as needed. Hence a wrapping component.

It’s very simple: the wrapper has a core.async channel that event listeners are writing to:

(defn draggable [cursor owner {:keys [build-fn id]}]
  (reify
    om/IInitState
    (init-state [_]
      {:mouse-chan (chan (sliding-buffer 100))
       :pressed false})
    om/IWillMount
    (will-mount [_]
      (let [mouse-chan (om/get-state owner :mouse-chan)]
        (go-loop []
          (let [[evt-type e] (<! mouse-chan)]
            (handle-drag-event cursor owner evt-type e))
          (recur))))
    om/IDidMount
    (did-mount [_]
      (let [node       (by-id id)
            mouse-chan (om/get-state owner :mouse-chan)]
        (events/listen node "mousemove" #(put! mouse-chan [:move %]))
        (events/listen node "mousedown" #(put! mouse-chan [:down %]))
        (events/listen node "mouseup" #(put! mouse-chan [:up %]))))
    om/IRenderState
    (render-state [_ {:keys [mouse-chan]}]
      (html
       (let [{:keys [top left]} (:position cursor)]
         [:div {:id id
                :style {:top (str (- top 40) "px") :left (str (- left 40) "px")
                        :position "absolute" :z-index 100}}
          build-fn])))))

Channel and default mouse pressed value are initialised in IInitState. Channel has a sliding buffer – this way when someone drags too fast we don’t update app state unnecessarily but drop the events instead.
In IDidMount we attach listeners to our component and “mousemove”, “mousedown” and “mouseup” events. The handler is simply putting a vector with the event type and the event object on the mouse-chan channel. Inside of IWillMount we have a go-loop that reads the messages and handles the events according to their type:

(defn handle-drag-event [cursor owner evt-type e]
  (when (= evt-type :down)
    (om/set-state! owner :pressed true))
  (when (= evt-type :up)
    (om/set-state! owner :pressed false))
  (when (and (= evt-type :move) (om/get-state owner :pressed))
    (om/update! cursor :position {:top (.-clientY e) :left (.-clientX e)})))

On mouse down and up, we update component’s local state accordingly – we don’t want to act on mouse move if the mouse is not pressed. On mouse move we simply update the cursor with x and y coordinates of the mouse, which causes the component to render at new position.

How does the draggable know what component to render? We pass the whole (om/build box (:draggable-box cursor)) as one of the options to draggable:


(defn draggable-widget [cursor owner]
  (reify
    om/IRenderState
    (render-state [_ state]
      (html
       [:div {:class "container"}
        (om/build w/draggable (:draggable-box cursor) {:opts {:id "box-widget"
                                                              :build-fn (om/build box (:draggable-box cursor))}})]))))

Here’s the working example. You’ve probably noticed that when you drag the component too fast, the cursor moves, but the component remains in the same place (buffer drops the events). It would be better to just take the last position of the cursor and move the component there. But I’ll let you figure this one out yourself ;-)

You can find the code in my GitHub repo. Let me know if you find bugs, or better yet, submit at PR!

Common mistakes to avoid when creating an Om component. Part 1.

For the past few months I’ve been creating various Om components and most of the time it goes smoothly. But sometimes I do something silly, and it’s not always obvious what it is. What’s obvious is that the UI doesn’t work and throws (sometimes cryptic) errors at me instead. Today I’m gathering what I remember into a tiny post. Maybe someone will find it helpful. And maybe I’ll finally remember those mistakes after putting them down on paper interwebs. One can hope!  :-)

  1. Form inside of another form.
    Error: Invariant Violation: findComponentRoot(..., .2): Unable to find element. This probably means the DOM was unexpectedly mutated (e.g., by the browser), usually due to forgetting a &lt;tbody&gt; when using tables or nesting &lt;p&gt; or &lt;a&gt; tags. Try inspecting the child nodes of the element with React ID ``. 

    Easy crime to commit if you’re moving some html tags around and leave one behind.
    GitHub issue here.

  2. Reading application state during user input, e.g. onClick, onChange. etc.
    Uncaught Error: Cannot manipulate cursor outside of render phase, only om.core/transact!, om.core/update!, and cljs.core/deref operations allowed.

    You need to deref to read the value:

    (:some-value @cursor)
  3. Same error as above but coming from your om/transact!
    Common mistake in the function that you pass to om/transact! is to do something like this:
    (om/transact! data :messages (fn [cursor] (conj (:messages data) some-value))

    Cursor is actually your (:messages data) so you just do:

    (fn [cursor] (conj cursor some-value))
  4. React.js < v. 0.11.0 Render method not returning a valid component
    Uncaught Error: Invariant Violation: ReactCompositeComponent.render(): A valid ReactComponent must be returned. You may have returned null, undefined, an array, or some other invalid object.

    You *must* return a single div. It might contain nested stuff, other divs, components, etc. but everything must be wrapped in a single div. And if you want to return nothing because you have no data to display, you still need to return an empty div from your render method.
    Starting from React 0.11.0 you are free to return null. Thanks @locks for pointing this out!

And an obvious tip: don’t use a minified build of React library for development if you want to see meaningful error descriptions.
I’ll add more pitfalls as I remember them.