Search code examples
clojurefunctional-programminghiccup

Building nested vector from map in Clojure


I have a set of URLs, and some of the URLs have indirect references (as a vector). Any URL that does not have an indirect reference just has nil. I'm starting out with the following test map:

{"URL 1" nil,
 "URL 2" ["indirect 1" "indirect 2"]}

I'm using hiccup to build an HTML report, so I want this output:

[:div "Imports: "
 [:ul
  [:li "URL 1"]
  [:li "URL 2"]
    [:ul
     [:li "indirect 1"]
     [:li "indirect 2"]
     [:li "indirect 3"]]]]

I'm running into some problems returning nil when the URL does not have an indirect reference. My current code looks like this:

(defn list-imports
  [imports]
  (if-not (nil? imports)
    [:div "Imports: "
     [:ul
      (for [direct (keys imports)]
        [[:li direct]
         (if-let [indirects (get imports direct)]
           [:ul
             (for [indirect indirects]
               [:li indirect])]
           [:span])])]]
    [:div "Imports: none" [:br] [:br]]))

The problem is, it's returning this...

[:div
 "Imports: "
 [:ul
  ([[:li "URL 1"] [:span]]
   [[:li "URL 2"] [:ul ([:li "indirect 1"] [:li "indirect 2"])]])]]

I had to add in a [:span] tag as the case for when the indirect imports are nil, which I don't really want there... but otherwise, it puts in nil there.

The other problem is that it ends up enclosed in () and an extra vector, because I'm doing multiple things within the for statement. When I try to convert it with hiccup, I get [:li "URL 1"] is not a valid element name.


Solution

  • This can be a tricky aspect of building hiccup tags. Sometimes it helps to break the problem into smaller pieces.

    (defn list-indirects
      [indirects]
      (when (seq indirects)
        [(into [:ul] (mapv (fn [i] [:li i]) indirects))]))
    
    (defn list-imports
      [imports]
      (if (some? imports)
        [:div "Imports: "
         (into [:ul]
           (for [[url indirects] imports]
             (into [:li url] (list-indirects indirects))))]
        [:div "Imports: none" [:br] [:br]]))
    

    These functions should give you the desired output.

    (list-imports {"URL 1" nil
                   "URL 2" ["indirect 1" "indirect 2"]})
    =>
    [:div "Imports: "
     [:ul
      [:li "URL 1"]
      [:li "URL 2"
       [:ul
        [:li "indirect 1"]
        [:li "indirect 2"]]]]]
    

    This output is slightly different from your expected output, but I think it's closer to what you'd actually want i.e. the [:li "URL 2"] tag in your example should contain the :ul of "indirects" to be valid HTML.

    Another thing to be wary of, if the order of these items is important, is that maps may not be ordered in the way you expect, especially once you have over a certain number of keys. When you traverse the map to build hiccup, it's possible that "URL 2" could come before "URL 1". You could workaround this by using a vector of tuples, or maybe a sorted map.