Anna Pawlicka

Programmer. Hiker. Cook. Always looking for interesting problems to solve.


About | Archive | Talks

Draggable wrapper component with Om and core.async

26 Sep 2014 | ClojureScript, Om, React

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!


Older · View Archive (25)

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!  :-)

Newer

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: