Search code examples
clojurescriptclojurescript-javascript-interop

Issue in Clojurescript application when changing class strings


I'm having a strange problem with a bit of Clojurescript in my application. I pulled all the relevant pieces out into a small Leiningen Reagent application. Here's the relevant Hiccup:

(defn current-page []
  (fn []
    (let [page (:current-page (session/get :route))]
      [:div
      [:table {:width "100%"}
        [:tbody
          [:tr {:class "row"}
            [:td "A"] [:td "B"] [:td "C"]]
          [:tr {:class "row"}
            [:td "A"] [:td "B"] [:td "C"]]
          [:tr {:class "row"}
            [:td "A"] [:td "B"] [:td "C"]]
          [:tr {:class "row"}
            [:td "A"] [:td "B"] [:td "C"]]
          [:tr {:class "row"}
            [:td "A"] [:td "B"] [:td "C"]]
          [:tr {:class "row"}
            [:td "A"] [:td "B"] [:td "C"]]                                                            
          [:tr {:class "row"}
            [:td "A"] [:td "B"] [:td "C"]]]]
      [:div {:onClick #(unselect-all-rows)} "Unselect All Rows..."]       
      [:div {:onClick #(select-all-rows)} "Select All Rows..."] ])))

It's just a simple HTML table. Here are the select and unselect functions:

(defn unselect-all-rows []
  (prn "Unselecting all.")
  (let [selected-rows (.getElementsByClassName js/document "row-selected")]
    (prn (str "Rows selected: " (.-length selected-rows)))
    (doall (map #(.remove (.-classList %) "row-selected") (array-seq selected-rows)))))

(defn select-all-rows []
  (prn "Selecting all.")
  (let [selectable-rows (.getElementsByClassName js/document "row")]
    (doall (map #(.add (.-classList %) "row-selected") (array-seq selectable-rows))))) 

The select-all-rows function works as expected, all rows get the "row-selected" class applied and the CSS highlights the rows:

enter image description here

But when I execute the unselect-all-rows function I only get SOME of the rows unselected:

enter image description here

If I click 2 more times then all the rows end up unselected. If I look at the console the number of rows being selected is what I expect, 7 in the first case, but it only seems to perform the remove operation on alternating rows:

enter image description here

What am I missing here?


Solution

  • When using reagent or any other kind of react wrapper your view function is the only thing that should be modifying the DOM. You don't do any direct DOM manipulation otherwise yourself. So you'd capture the state of the selected rows somewhere (either a local atom or some other kind of managed state, eg. re-frame).

    So a simple version could look like:

    (defn current-page []
      (let [toggle-ref (r/atom false)
            select-all #(reset! toggle-ref true)
            deselect-all #(reset! toggle-ref false)]
        (fn []
          (let [page (:current-page (session/get :route))
                row-class (if @toggle-ref "row-selected" "row")]
            [:div
             [:table {:width "100%"}
              [:tbody
               [:tr {:class row-class}
                [:td "A"] [:td "B"] [:td "C"]]
               [:tr {:class row-class}
                [:td "A"] [:td "B"] [:td "C"]]
               [:tr {:class row-class}
                [:td "A"] [:td "B"] [:td "C"]]
               [:tr {:class row-class}
                [:td "A"] [:td "B"] [:td "C"]]
               [:tr {:class row-class}
                [:td "A"] [:td "B"] [:td "C"]]
               [:tr {:class row-class}
                [:td "A"] [:td "B"] [:td "C"]]
               [:tr {:class row-class}
                [:td "A"] [:td "B"] [:td "C"]]]]
             [:div {:onClick deselect-all} "Unselect All Rows..."]
             [:div {:onClick select-all} "Select All Rows..."]]))))
    

    I didn't actually test this code and in reality you probably want to store which rows are actually selected but it should give you an idea how this is supposed to work.