Make clj-ajax/GET result in downloading the resource sent by a ring response?

I have a http-kit web server where I define this (test) route with compojure:

(defroutes routes
  (GET
   "/csv" request
   (->
    (assoc
     (response/response "a,b")
     :headers
     {"Content-Type"        "text/csv; application/octet-stream"
      "Content-Disposition" "attachment; filename=file.csv"}))))

O the re-frame frontend, I have a link with a on-click calling cljs-ajax GET:

:on-click #(GET "/csv" {:params param})

When I hit http://localhost:3000/csv, it works, the file gets downloaded. But when I click on the frontend button, it does not work, the response is displayed in the console but the file does not get downloaded.

I’ve explored possible solutions with :response-format within the GET sexp, but was not able to sort this. I’m pretty sure I’m missing something obvious, but like many obvious things, I’m probably too close to notice it (yeah, I’ve spent some time on this!)

If anyone is able to push me in the right direction or to provide a working minimal example, I’d be very grateful! Thanks a lot.

Hi @bzg,

Here’s a function that might work for you. It creates an anchor element and simulates a click. You’ll have to get the response from your GET invocation and grab the appropriate headers.

(defn download-file!
  [data content-type file-name]
  (let [data-blob (js/Blob. #js [data] #js {:type content-type})
        link (js/document.createElement "a")]
    (set! (.-href link) (js/URL.createObjectURL data-blob))
    (.setAttribute link "download" file-name)
    (js/document.body.appendChild link)
    (.click link)
    (js/document.body.removeChild link)))

An example invocation with data from your example:

(download-file! "a,b", "text/csv" "file.csv")

If anyone has a better way to handle this on the frontend I’d also like to know.
Thanks.

Amar

Can’t you just make it a regular link, like this?

[:a :href "/csv"]

Yes that should work for the OP. I had assumed the XHR was a requirement.

Yes, indeed, this is what I ended up doing.

The thing is I had to add the query parameters to the regular link, which looks hackish to me.

I’m still interested on what is the expected way of dealing with this general issue (i.e. send a cljs-ajax request to a server and download a file as a result.)

Thanks to both of you for taking the time to answer this already!

Here’s a more complete example using re-frame and re-frame-http-fx. Clicking the Download button will fetch the json response and save it to a file. Hope that helps.

(ns ajax-file-download-example
  (:require [day8.re-frame.http-fx]
            [ajax.protocols]
            [re-frame.core :as re-frame]))

(defn download-file!
  [data content-type file-name]
  (let [data-blob (js/Blob. #js [data] #js {:type content-type})
        link (js/document.createElement "a")]
    (set! (.-href link) (js/URL.createObjectURL data-blob))
    (.setAttribute link "download" file-name)
    (js/document.body.appendChild link)
    (.click link)
    (js/document.body.removeChild link)))

(re-frame/reg-event-fx
 :download-file-ex
 (fn [_world [_ _response]]
   (js/console.log "error encountered")
   {}))

(re-frame/reg-event-fx
 :download-file-ok
 (fn [_world [_ response]]
   (download-file! response "application/json" "data.json")
   {}))

(re-frame/reg-event-fx
 :download-file
 (fn [_world _]
   {:http-xhrio {:method          :get
                 :uri             "https://api.github.com/orgs/day8"
                 :response-format {:description "file-download"
                                   :content-type "*/*"
                                   :type :blob
                                   :read ajax.protocols/-body}
                 :on-success      [:download-file-ok]
                 :on-failure      [:download-file-ex]}}))

(defn panel
  []
  [:button {:on-click #(re-frame/dispatch [:download-file])} "Download"])

1 Like

Thank you very much!