4

I'm displaying a menu in Om, using a component and subcomponent like this:

(def app-state (atom {:location ""
                      :menuitems [["Pages" "/pages/"]
                                  ["Images" "/images/"]]}))

(defn menu-item-view [parent-cursor item owner]
  (reify
    om/IRender
    (render [this]
      (dom/li #js {:className (if (= (:location @app-state) (last item)) "active" "inactive")} 
        (dom/a #js 
               {:onClick (fn [_] (swap! app-state assoc :location (last @item)))} 
               (first item))))))

(defn menu-view [app owner]
  (reify
    om/IRender
    (render [this]
      (dom/li #js {:className "has-dropdown not-click"}
        (dom/a nil "Menu")
        (apply dom/ul #js {:className "dropdown"}
          (om/build-all (partial menu-item-view app) 
                        (:menuitems app)))))))

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

My question is how do I update the (@app-state :location) and correctly rerender the menu?

The update in the code above:

(swap! app-state assoc :location (last @item))

does work, but the tree is not updated correct.

I suspect i need to use om/update! or om/transact! but they take a cursor and the only cursor i have in menu-item-view is to the current menu item, not the full app-state. So i cannot access :location.

How is this handled?

I would prefer to aviod core.async and channels for the time being if possible.

Drewes
  • 2,581
  • 1
  • 18
  • 18
  • add a callback function and pass it to subcomponent in opts. Call it to modify parent component state. – edbond Oct 14 '14 at 14:31

2 Answers2

6

Now that we have reference cursors you could probably do something like this:

(def app-state (atom {:location ""
                      :menuitems [["Pages" "/pages/"]
                                  ["Images" "/images/"]]}))

(defn location []
  (om/ref-cursor (:location (om/root-cursor app-state))))

(defn menu-item-view [item owner]
  (reify
    om/IRender
    (render [this]
      (let [x (location)]
        (dom/li #js {:className (if (= x (last item)) "active" "inactive")}
                (dom/a #js
                       {:onClick (fn [_] (om/update! x (last @item)))}
                       (first item)))))))

(defn menu-view [app owner]
  (reify
    om/IRender
    (render [this]
      (dom/li #js {:className "has-dropdown not-click"}
              (dom/a nil "Menu")
              (apply dom/ul #js {:className "dropdown"}
                     (om/build-all menu-item-view (:menuitems app)))))))

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

It's just an idea - I haven't actually tested it.

Anna Pawlicka
  • 757
  • 7
  • 22
2

Yes, all updates should occur through om/transact! or om/update!.

You could pass the main cursor to the controls state in :init-state or :state. This would give you access to it for update.

Alternatively, you could avoid using om/build-all and use build directly to pass multiple cursors to the control as specified here.

Simply call the following instead:

(map #(om/build menu-item-view {:main-cursor app :menu-cursor %}) (:menuitems app)) 
Mongus Pong
  • 11,337
  • 9
  • 44
  • 72