Skip to content

Instantly share code, notes, and snippets.

@kennethkalmer
Created July 7, 2017 08:25
Show Gist options
  • Save kennethkalmer/e74794f7b8f6e3a695a1ddabfc38f2e3 to your computer and use it in GitHub Desktop.
Save kennethkalmer/e74794f7b8f6e3a695a1ddabfc38f2e3 to your computer and use it in GitHub Desktop.

Upload component

Context

Based heavily on [s3-beam][1], but uses re-frame events/subs to get the job done. The /sign handler is the [s3-beam][1] handler (near verbatim).

For background on (ui.core/component "...") see https://opensourcery.co.za/2017/02/12/using-semantic-ui-react-with-re-frame/

ui.ajax is just thin wrappers and/or aliases around plumbing from ajax.core from cljs-http

Problem

Although the uploading works great in isolation, the handler that kicked off the process needs access to the storage-key value returned from the object storage so it can pass that along to the backend.

Using async-flow-fx this doesn't seem possible.

[1] https://github.com/martinklepsch/s3-beam

(ns ui.uploads
(:require-macros [reagent.ratom :refer [reaction]])
(:require [re-frame.core :as re-frame]
[reagent.ratom :as ratom]
[ui.core :as ui]
[ui.ajax :as ajax]))
(def interceptors
[(when ^boolean js/goog.DEBUG re-frame/debug)
re-frame/trim-v])
(re-frame/reg-event-fx
:upload/start
interceptors
(fn [{db :db} [identifier file]]
(let [progress {:bytes-sent 0
:bytes-total (.-size file)}]
{:db (assoc-in db [::progress identifier] progress)
:dispatch (if (some? file)
[::get-upload-url identifier file]
[:upload/done identifier])})))
(re-frame/reg-event-fx
::get-upload-url
interceptors
(fn [_ [identifier file]]
(let [file-name (.-name file)
mime-type (.-type file)]
{:http-xhrio {:method :post
:uri "/sign"
:params {:file-name file-name
:mime-type mime-type}
:format (ajax/transit-request-format)
:response-format (ajax/transit-response-format)
:on-success [::upload-file identifier file]
:on-failure [:upload/failed identifier]}})))
(defn upload-request-body [file form-data]
(let [data (assoc form-data :file file)
fd (new js/FormData)]
(doseq [[k v] data]
(.append fd (name k) v))
fd))
(re-frame/reg-event-fx
::upload-file
interceptors
(fn [{db :db} [identifier file upload-url-response]]
(let [upload-url (:upload-url upload-url-response)
form-data (:form-data upload-url-response)
storage-key (:key upload-url-response)
request-body (upload-request-body file form-data)]
;;
;; `storage-key` needs to be available to whoever needs it after upload completes
;;
{:db (update-in db [::progress identifier] assoc :key storage-key)
:http-xhrio {:method :post
:uri upload-url
:body request-body
:response-format (ajax.core/raw-response-format)
:on-success [:upload/done identifier storage-key]
:on-failure [:upload/failed identifier]
:progress-handler #(re-frame/dispatch [::upload-progress
identifier
%])}})))
(re-frame/reg-event-db
:upload/done
interceptors
(fn [db [identifier]]
(update db ::progress dissoc identifier)))
(re-frame/reg-event-db
::upload-progress
interceptors
(fn [db [identifier progress-event]]
(let [progress {:bytes-sent (.-loaded progress-event)}]
(update-in db [::progress identifier] merge progress))))
(re-frame/reg-sub
::upload-progress
(fn [db [_ identifier]]
(get-in db [::progress identifier])))
(defn attachment [{:keys [identifier label file-selected]
:or {:file-selected identity}
:as options}]
(let [input (ui/component "Form" "Input")
button (ui/component "Button")
progress-bar (ui/component "Progress")
file-id (gensym "attachment-")
filename (ratom/atom "")
progress (re-frame/subscribe [::upload-progress identifier])
uploading? (reaction
(some? (:bytes-sent @progress)))
completed? (reaction
(and @uploading?
(= (:bytes-sent @progress) (:bytes-total @progress))))
percent (reaction
(* (/ (:bytes-sent @progress) (:bytes-total @progress))
100))
choose-file (fn [e d]
(-> (.getElementById js/document file-id)
(.click)))
file-changed (fn [e]
(let [files (-> e .-target .-files)
file (aget files 0)
name' (.-name file)]
(reset! filename name')
(if (fn? file-selected)
(file-selected identifier file))))]
(fn [options]
(if @uploading?
[:> progress-bar {:indicating true
:percent @percent}]
[:> input {:action true
:label label}
[:input {:placeholder "File"
:read-only true
:value @filename
:on-click choose-file}]
[:input {:id file-id
:type "file"
:style {:display "none"}
:on-change file-changed}]
[:> button {:icon "attach"
:content "Choose"
:type "button"
:on-click choose-file}]]))))
(ns somewhere
(:require [re-frame.core :as re-frame :refer [reg-event-fx dispatch]]
[reagent.ratom :as r]
[ui.uploads :as uploads]))
(defn some-form []
(let [form-data (r/atom {})
file-selected #(swap! form-data merge {:attachment/identifier %1
:attachment/source %2})
on-submit #(dispatch [:something/create @form-data])]
(fn []
[:form {:on-submit on-submit}
[uploads/attachment
{:identifier (gensym "attachment-")
:label "File"
:file-selected file-selected}]])))
(defn upload-flow [identifier file params]
{:first-dispatch [:upload/start identifier file]
:rules
[{:when :seen?
:events [(fn [[e i _]]
(= [e i] [:upload/done identifier]))]
:dispatch [::create-thing params]}
{:when :seen?
:events [::thing-created]
:halt? true}
{:when :seen-any-of?
:events [[:upload/failed identifier] ::request-failed]
:halt? true}]})
;; This event handler just prepares and coordinates
(reg-event-fx
:something/create
interceptors
(fn [{db :db} [params]]
(let [identifier (:attachment/identifier params)
file (:attachment/source params)
thing (dissoc params :attachment/source :attachment/identifier)]
{:db (assoc db :loading true)
:async-flow (upload-flow identifier file thing)})))
(reg-event-fx
::create-thing
interceptors
(fn [{db :db} [thing]]
;;
;; Here we need access to the storage-key value from the upload process :(
;;
{:http-xhrio {:method :post
:uri "/api/things"
:params thing
:format (transit-request-format)
:response-format (transit-response-format)
:on-success [::thing-created]
:on-failure [::request-failed]}}))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment