HTML Table with rowspan


#1

I have a structure like the following JSON

{
    a: [ {b, c, d},
          {e, f, g}
          {h, i, j} ],
    k: [ {l, m, n},
          {o, p, q},
          {r, s, t}]
}

I want to generate a table that ends up like

    | b | c | d
   a| e | f | g
    | h | i | j

    | l | m | n
   k| o | p | q
    | r | s | t

So “a” and “k” need to have rowspan 3. I can’t seem to figure this one out. With hiccup, every function call can only return a single element so I can’t figure out how to get three rows where the first one is a different length than the rest.

The final HTML should be like

<table>
    <tbody>
       <tr><td rowspan=3>a</td>
           <td>b</td>
           <td>c</td>
           <td>d</td>
       <tr><td>e</td>
           <td>f</td>
           <td>g</td>
       <tr><td>h</td>
           <td>i</td>
           <td>j</td>

       <tr><td rowspan=3>k</td>
            <td>l</td>
            <td>m</td>
            <td>n</td>
       <tr><td>o</td>
            <td>p</td>
            <td>q</td>
       <tr><td>r</td>
            <td>s</td>
            <td>t</td>
    </tbody>
</table>

#2

Are you referring to the hiccup-like syntax in Reagent, or actual HTML rendering using the hiccup library? The single element limitation only applies to Reagent (really React).

In either case, a seq will be expanded into the children, which should support map and for expressions:

(def data
  {:a [[:b :c :d]
       [:e :f :g]
       [:h :i :j]]
   :k [[:l :m :n]
       [:o :p :q]
       [:r :s :t]]})

I converted the data from your provided JSON (which was invalid) into Clojure data structures.

(defn section [[k rows]]
  (for [[idx row] (map-indexed vector rows)]
    [:tr
     (if (zero? idx)
       [:td {:rowspan (count rows)} k])
     (for [col row]
       [:td col])]))

A common pattern with hiccup/reagent is to use for with (map-indexed vector coll) which gives you a seq of index/value pairs, which I destructure with [idx row]. If the index is zero, also add the rowspan element.

(defn my-fn []
  (html
    [:table (mapcat section data)]))

You’re probably familiar with the map function which takes a function as the second argument and calls it for each element and returns as a seq. The mapcat function will do the same, but if the fn returns a seq it will concat them together into a single seq. This is similar to flatMap in other languages.

The for macro and mapcat function return sequences, where hiccup will take each item and consider it a child node on the parent [:tag …] form.


#3

Since you seem to be well versed in python based on your other posts, I should also mention another way to handle this:

(defn section [[k rows]]
  (for [idx (range 0 (count rows))]
    [:tr
     (if (zero? idx)
       [:td {:rowspan (count rows)} k])
     (for [col (rows idx)] ; Equivalent to (get rows idx)
       [:td col])]))

Iterating an index from range 0 to N and using that to lookup an item is common in python and many other languages.


#4

I don’t like loops, I only like sequential structures. :wink:

(def data
  {:a [[:b :c :d]
       [:e :f :g]
       [:h :i :j]]
   :k [[:l :m :n]
       [:o :p :q]
       [:r :s :t]]})

(defn f1 [[k v]]
  (let [[h & t] v
        f   (fn [x] (mapv #(vector :td %) x))
        tds (map #(->> % f (into [:tr] ,)) t)]
     (->> (f h)
          (into [:tr [:td {:rowspan (count v)} k]] ,)
          (conj tds ,))))
          
(defn f2 [x]
  (->> (reduce #(->> %2 f1 (into %1 ,)) [:tbody] x)
       (conj [:table] ,)
       doall))
  
(f2 data)

;return

[:table 
 [:tbody 
  [:tr 
   [:td {:rowspan 3} :a] 
   [:td :b] 
   [:td :c] 
   [:td :d]] 
  [:tr 
   [:td :e] 
   [:td :f] 
   [:td :g]] 
  [:tr 
   [:td :h] 
   [:td :i] 
   [:td :j]] 
  [:tr 
   [:td {:rowspan 3} :k] 
   [:td :l] 
   [:td :m] 
   [:td :n]]
  [:tr 
   [:td :o] 
   [:td :p] 
   [:td :q]] 
  [:tr 
   [:td :r] 
   [:td :s] 
   [:td :t]]]]

#5

If you’re familiar with Django for rendering HTML templates, you might find Selmer to your liking. It’s what we use at work for all of our server-side rendered HTML pages and our HTML emails.