Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cojurescript Om: handling local state changes in different components

I am building an om based form were subsections may be viewed either collapsed or expanded. The view status is saved in the subsections local state:

(defn subsection-view [subsection owner]
  (reify
     om/IInitState
     (init-state [this]
        {:collapsed true}))

The problem is that each subsections view status can be effected both ways either by the collapse-expand-all button or by a separate button displayed for each subsection.

In order to handle expand-compress-all there is a global collapse status saved in the form local state:

(defn form-view [data owner]
  (reify
    om/IInitState
    (init-state [this]
       {:all-collapsed true})))

Obviously both buttons on-click events are handled by updating the collapse status in the local state.

(om/update-state! owner :collapsed not)

My question is how should I know which status was updated last in order to display the right view?

Or where is the right place (local state or application state) to save the collapse status that might be effected from different triggers in different levels of the component tree?

like image 486
naomi Avatar asked Sep 28 '22 15:09

naomi


2 Answers

I am a fan of core.async and I would implement it using channels. I would have the Sections listen for a Collapse/Expand Message and when it comes change the local state. The local state change would cause a re-draw. Expanding a single Section only updates that section's local state.

like image 57
Chaos Rules Avatar answered Oct 08 '22 03:10

Chaos Rules


If you are thinking about time and causality in order to "display the right view" your are missing React/Om's point. You should not make that decision. You should just link the display to one piece of state and make sure that piece of state is right. As Chaos Rules said, core.async is the way to go.

Since the field components are already coupled to something external (the collapse all button) I would only model collapsed inside the form's local state/ Then I would provide a channel collapse-ch to the fields so that they can communicate back when they were clicked. Finally I would set an event handler in IWillMount to listen for those clicks:

(def init-state [true true true])

(defn form-view [data owner]
  (reify
    om/IInitState
    (init-state [_]
      {:collapsed init-state 
       :collapse-ch (chan)})
    om/IWillMount
    (will-mount [_]
      (let [collapse-ch (om/get-state owner :collapse-ch)]
        (go (loop []
              (let [index (<! collapse-ch)]
                (om/update-state! owner [:collapsed index] not))
              (recur)))))
    om/IRenderState
    (render-state [_ {:keys [collapsed collapse-ch]}]
      (dom/div nil
               (dom/button
                #js {:onClick (fn [_]
                                (om/set-state! owner :collapsed init-state))}
                "Collapse All")
               (apply dom/div nil
                      (map #(om/build field-view {:collapsed? %1}
                                      {:init-state {:index %2
                                                    :collapse-ch collapse-ch}})
                           collapsed
                           (range)))))))

As for the fields, they just need to put their index on the channel whenever they are clicked:

(defn field-view [data owner]
  (reify
    om/IRenderState
    (render-state [_ {:keys [index collapse-ch]}]
      (dom/button #js {:onClick (fn [_]
                                  (go (>! collapse-ch index)))}
                  (if (:collapsed? data)
                    "Collapsed"
                    "Showing")))))

In case I missed something, the full example is here. It looks like a lot, but in my experience it is the best way to achieve a sound separation of concerns. There is another similar example in the tutorial.

like image 45
sbensu Avatar answered Oct 08 '22 03:10

sbensu