Fork me on GitHub
#fulcro
<
2022-09-30
>
Sam Mokracek08:09:51

Hi there! I’ve making a Connect-4 game as a way to learn Fulcro and Clojure[script] the past couple weeks, and I’m currently working on getting my first mutation working. My basic component structure is a row of buttons above a row of columns, consisting of cells. My mutation is triggered by a button click, which passes a 0-6 id to identify which column to drop the coin into. A color is passed as well, in theory from the root. The intent is that the mutation places the coin at the lowest non-occupied blank cell.

(defn create-column*
  "Creates a new column given the old one and a coin to drop. Returns nil if the
   old column was full"
  [state-map column-id color]
  (let [column (column-id :board/cols)]
    (if (= (nth column 0) :blank)
      (let [cell (->> column
                      (filter (fn [c] (= (:cell/color c) :blank)))
                      first)]
        (assoc-in state-map [(-> (:board/cols column-id) (:cell/id cell))]
                  {:cell/id (:cell/id cell) :cell/color color}))
      nil)))

(defmutation add-coin
  "Adds a coin of specified color to bottom-most available blank cell"
  [{column-id :column/id
    color :cell/color}]
  (action [{:keys [state]}]
          (swap! state update-in [:board/cols] (create-column* state column-id color))))
Then in my components:
(defsc Button
  [this {:button/keys [id] :as props} {:keys [onClick]}]
  {:query [:button/id]
   :ident (fn [] [:button/id id])
   :initial-state (fn [id] {:button/id id})}
  (button {:id id
           :style {:margin-bottom "10px"
                   :width "40px"
                   :height "40px"
                   :border-radius "100%"
                   :border "solid"
                   :border-width "2px"}
           :onClick #(onClick id :root/turn)} "▼"))

(def ui-button (comp/factory Button {:keyfn :button/id}))

; ...

(defsc CellColumn
  "Composed as a column 6 Cells"
  [this {:column/keys [id cells] :as props}]
  {:query [:column/id
           {:column/cells (comp/get-query Cell)}]
   :ident (fn [] [:cell/id (:cell/id props)])
   :initial-state (fn [{:keys [id]}]
                    {:column/id id
                     :column/cells
                     [(comp/get-initial-state Cell {:id 0 :color :blank})
                      (comp/get-initial-state Cell {:id 1 :color :blank})
                      (comp/get-initial-state Cell {:id 2 :color :blank})
                      (comp/get-initial-state Cell {:id 3 :color :blank})
                      (comp/get-initial-state Cell {:id 4 :color :blank})
                      (comp/get-initial-state Cell {:id 5 :color :blank})]})}
  (let [add-coin (fn [color]
                   comp/transact! this [(api/add-coin
                                         {:column/id id :cell/color :root/turn})])]
    (div (map #(ui-cell (comp/computed % {:onClick add-coin})) cells))))

(def ui-cell-column (comp/factory CellColumn {:keyfn :column/id}))

; ...

(defsc Root
  "Overall page layout is composed here. Background color is done in the root HTML div."
  [this {:keys [board turn moves button-row]}]
  {:query [{:board (comp/get-query Board)}
           :turn :moves {:button-row (comp/get-query ButtonRow)}]
   :initial-state (fn [_] {:board (comp/get-initial-state Board)
                           :turn :red
                           :moves 1
                           :button-row (comp/get-initial-state ButtonRow)})}
  (div {:style {:display "flex"
                :flex-direction "column"
                :height "auto"
                :margin "10px"
                :align-items "center"
                :font-family "sans-serif"}}
       (h1 {:style {:font-size "40pt"}}
           "Welcome to Connect4!")
       (ui-button-row button-row)
       (ui-board board)
       (div {:style {:display "flex"
                     :flex-direction "row"}}
            (h2 {:style {:margin-right "5px"}}
                (str "It's " (case turn
                               :red "Red"
                               :yellow "Yellow"
                               "ERROR")) "'s turn!")
            (h2 {:style {:margin-left "5px"}}
                (str "Move: " moves)))))
I’m pretty confused as to how my add-coin mutation can access the turn prop at root. More importantly, am I on the right track at all with this? I’d appreciate any help!

tony.kay18:10:40

Root is special, in that it does not have an ident. Any keys there are in the root of the db, not in a table. In general, the helpers in Fulcro work on normalized things, so root is an outlier. As a result, I usually treat root as a simple container for the rest of the UI. I.e. I’d make a Game component that is a child of root, and IT would have turn, columns, etc. You did not show your board, but is should also have an ident, otherwise the state is not normalized and it will be a pain to do anything. If it DOES have an ident, then you mutation is wrong, in that it isn’t changing data for a component, but at the root. state is the ENTIRE normalized state database, NOT the state of a component. There is a :ref entry in env that IS the ident of the component that called transact! (if there was one), which can be useful, but you have to be careful because that introduces a coupling that may not be desirable (it is intended for mutations that are always implicitly meant to affect this ). You should definitely be looking at the book and video tutorials. It’s simpler than you think, but misconceptions are common.

Jakub Holý (HolyJak)20:10:19

I would add that personally I would make Button be a stateless, https://blog.jakubholy.net/2020/fulcro-divergent-ui-data/#_a_ui_only_component because it does not correspond to any real data entity. The DB Explorer in fulcro inspect tools is very good for seeing what props Root can see and for checking that things are well connected. I show CellColumn's ident as (fn [] [:cell/id (:cell/id props)]) which could be simplified using the https://book.fulcrologic.com/#_template_idents to :cell/id . But it looks wrong, I guess you meant :column/id

Sam Mokracek22:10:54

Thank you for these replies. I’ve been working off the book, and getting my head around the graph db and other concepts has taken some time. Fulcro seems very cool and I’d like to learn it well.