Clojurescript Reagent Image-Previewing Selector

Table of Contents

Intro

img

It took some elbow-grease but I was able to convert online examples1 into a working image selector in Reagent Clojurescript. It isn’t a lot of code, but there were two complications: converting the declarative style of the examples into more functional CLJS (hint: it involves closures), and doing it in a way that will jive with the React life-cycle.

CSS code

The following styling makes things look okay. This is using Garden2 syntax. I used it on the front-end, but it would work just as well anywhere else.

(def display-image
  [:.cntnr
   {:width "400px"
    :height "225px"
    :display "flex"
    :justify-content "center"
    :align-items "center"
    :flex-direction "column"
    :gap "10px"
    :border "1px solid black"}

   [:.display-image
    {:width "300px"
     :height "169px"
     :border "1px solid black"
     :background-position "center"
     :background-size "cover"}]])

The Rest: Clojurescript Reagent

Since this was work heavily involving the DOM and Browser, I made use of console logs as I figured things out I have left them in place, commented out, for the reader’s benefit.

Clojure doesn’t do code hoisting3, so the change-read function needs to be defined before it is used. This is the one that handles the user event and the image that was chosen.

(require '(reagent [core :as r]))
(defn change-read
  "Called on the change event of a file input, operate on the selected file"
  [clicked UPLOADED-IMAGE]
  (let [reader (js/FileReader.)
        set-image (fn [loade]
                    (reset! UPLOADED-IMAGE (-> loade
                                                    .-target
                                                    .-result))
                    ;; (js/console.log (str "upped is the image data at this point in the thread: >>>"))
                    ;; (js/console.log @uploaded-image)
                    )
        file (first (array-seq (.. clicked -target -files)))
        image-path (-> clicked .-target .-value)]
    ;; (js/console.log (str "Image file is >>> "))
    ;; (js/console.log file)
    (.addEventListener reader "load" set-image)
    (.readAsDataURL reader file) ;; this triggers the "onload" event
    ;; (js/console.log "image path is >>>")
    ;; (js/console.log image-path)
    ))

Next we create the actual React and DOM component that will utilize the functionality we just implemented. Here we leverage a form-2 Reagent structure to create an atom in a closure, so that changing the reagent atom will not trigger a refresh of the atom definition itself4.

(defn image-selector
  "an image-selection element that shows the image you've chosen.

  This is a form-2 reagent component so we can avoid global state.
  https://github.com/reagent-project/reagent/blob/master/doc/CreatingReagentComponents.md#form-2--a-function-returning-a-function"
  []
  (let [UPLOADED-IMAGE (r/atom nil)
        bg-image-style (fn []
                             (if-let [i @UPLOADED-IMAGE]
                               {:background-image
                                (str "url(" i ")")}
                               {:background-color "red"}))]
    (fn []
      [:div.cntnr
       [:input {:type "file"
                :id "image-in"
                :on-change #(change-read % UPLOADED-IMAGE)
                :accept "image/*"}]
       [:div.display-image {:style (bg-image-style)}]])))

The actual displaying of it

The way of actually presenting the data was new to me before this little project, and I was pleased that it worked so niceley with Reagent. Essentially, once we have obtained the image from the user, we display it as a CSS background image using the data-uri5. I have utilized datauris for other purposes before, but not within CSS 6.

bg-image-style (fn []
                     (if-let [i @UPLOADED-IMAGE]
                       {:background-image
                        (str "url(" i ")")}
                       {:background-color "red"}))

It turns out that background-image can take data-urls, the mumbo-jumbo reminiscent of the raw characters you see when you open an image in a text editor.

Conclusion

I made this work on a branch of my site7, which is in ShadowCLJS. It isn’t actually one of the places this is needed, but was the easiest testbed. Next up, perform the actual upload of the selected image, which is something my site is NOT convenient for.

Footnotes

1 MDN is my favorite technical explanation of the syntax, at https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications

2 Garden is a superb CSS solution, allowing you to represent your CSS as data-structures and with full language facilities. I’ve written more on this at https://tech.toryanderson.com/2020/03/14/my-garden-css-has-ascended/

3 hoisting would and REPLs don’t go together very well. Rich Hickey has an excellent message about this on an old email thread, but I am failing to find it now. It was probably on Hacker News, back when Clojure was first developed.

4 Reagent refreshing the atom you are trying to change is a common cause of “my app isn’t working!” frustration, for instance on text inputs that don’t seem to be receiving your keyboard typing. In those cases, they actually are receiving it, but “on-change” they are re-emptying the containing atom so quickly you don’t even see your text going in.

5 The good place on data URIs https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs .

6 url() is actually a CSS function. Apparently even the in-line data usage with a data-uri has apparently been around since the dawn of time. This MDN entry actually has lots of great examples in addition to the technical details. https://developer.mozilla.org/en-US/docs/Web/CSS/url()

7 See the code in place here: https://gitlab.com/toryanderson/toryanderson.com/-/blob/image/src/main/toryanderson/views/main.cljs#L145 . Uploading will have to be done on one of our full-stack apps, as opposed to my completely client-side site.

Tory Anderson avatar
Tory Anderson
Web App Engineer, Digital Humanist, Researcher, Computer Psychologist