Next, we will build multiple column-view components, one per column. Each of them receives the list of cards from the application state as their shared state. We will use that to retrieve the card details from the IDs we store in each column.
We also pass transfer-chan to each column, since all of the columns need a reference to it:
(defui Column
static om/Ident
(ident [this {:keys [db/id]}]
[:column/by-id id])
static om/IQuery
(query [this]
[:db/id :column/title {:column/cards [:db/id :card/name :card/description]}])
Object
(render [this]
(let [{:keys [db/id column/title column/cards transfer-chan] :as data} (om/props this)]
(dom/div #js {:className "column"
:onDragOver #(.preventDefault %)
:onDrop #(handle-drop % transfer-chan id)}
(dom/div #js {:className "column-title"} title)
(if cards
(map #(card-view % id) cards))))))
(def column* (om/factory Column ))
(defn column [props transfer-chan]
(column* (assoc props :transfer-chan transfer-chan)))
(defn column-view [columns transfer-chan]
(apply dom/div nil
(map #(column % transfer-chan) columns)))
We build column-view by mapping data for each factory column and passing the transfer channel as a property.
The om/IQuery protocol looks a bit different than what we have seen before. On top of asking about the column data, such as id and title, we want to obtain cards information, including card ID, name, and description.
The component view code should look fairly familiar at this point. We add CSS classes for each column and its title. We put the styles in resources/public/css/style.css:
.column {
width: 30%;
float: left;
border: 1px solid;
padding: 10px;
}
.column-title {
font-weight: bold;
margin-bottom: 10px;
}
.card {
border: 1px solid;
margin: 10px 0;
padding: 5px;
}
Each column responds to drag and drop behavior. The onDrop JavaScript event is fired by the browser when a user drops a draggable DOM element onto this component. handle-drop takes care of this, like so:
(defn- handle-drop [e transfer-chan destination-column-id]
(.preventDefault e)
(let [data {:card-id (js/parseInt (get-transfer-data! e "cardId"))
:source-column-id (get-transfer-data! e "sourceColumnId")
:destination-column-id destination-column-id}]
(put! transfer-chan data)))
This function creates the transfer data—a map with the keys :card-id, :source-column-id, and :destination-column-id—which is everything we need to move the cards between columns. Finally, we put! it into the transfer channel.
Next, we build a number or card-view component:
(defn- card-view [{:keys [db/id card/name card/description]} column-id]
(dom/div #js {:key id
:className "card"
:draggable true
:onDragStart (fn [e]
(set-transfer-data! e "cardId" id)
(set-transfer-data! e "sourceColumnId" column-id))}
(dom/div nil name)
(dom/div nil description)))
We make it draggable and install an event handler on the onDragStart event, which will be triggered when the user starts dragging the card.
This event handler sets the transfer data, which we use from handle-drop.
We have glossed over the fact that these components use a few utility functions. That's OK, as we will now define them in a new namespace.