A message takes attachments, a project its files. co.multiply.tropical.upload has the browser send a file's bytes
straight to where the app keeps them, such as a bucket taking presigned PUTs, so they never pass through the app, while
the island renders the files and their progress. Datastar's own binding of a file input, which puts the bytes in
signals as base64, suits a file small enough for an action's body; this is for the rest.
(defisland composer
[]
(let [{:keys [uploads start remove! take!]}
(upload/use-uploads :attachments
{:target (fn [{:keys [id type size]} uploads]
(if (< size max-size)
(let [key (str "pending/" id)]
{:ref key :url (presign-put key) :headers {"Content-Type" type}})
{:refused "Too large."}))
:arrived (fn [{:keys [ref size]}]
(when-not (= size (stored-size ref))
{:refused "It didn't arrive whole."}))})
send (use-action :send (fn [{:strs [text]}] (post-message! text (map :ref (take!)))))]
[:form {:data-on:submit send}
[:textarea {:data-bind "text"}]
[:input {:type "file" :multiple true :data-on:change (start "evt.target.files")}]
(for [{:keys [id name state reason]} uploads]
[:div name
(case state
:uploading [:div {:data-style:width (str "(100 * " (upload/progress id) ") + '%'")}]
:arrived "Attached"
reason)
[:button {:type "button" :data-on:click (use-action [:remove id] (fn [_] (remove! id)))} "Remove"]])]))
:target answers where its
bytes go, a URL, a method (PUT unless given) and headers, or refuses it. The browser sends them there, three files
at a time, and reports how it went.:target also returns the app's own reference, such as the storage key, which
stays on the server: the browser knows an upload by an id the island gave it. Its reports go to a token bound to the
user and revoked with the island, and reach that island's uploads only, so a client can't make the app take an
object of its choosing, or another session's. That a file arrived is still the browser's word: :arrived can
check, as with a HEAD, and refuse it.:target has answered, so the island shows it at once,
and the island renders again as the list changes. An upload is :uploading, :arrived, :refused or :failed,
the last two with a :reason. :target sees the list as it stands, the pick's earlier files included, one pick of
the island at a time, so a limit holds.take! removes the uploads that arrived and returns them, as a message being sent takes its
files, and remove! drops one.:settled gets a pick at once: called once nothing is uploading, with the uploads that arrived,
which it takes. A pick made while another is on its way joins its batch, and one that failed or was refused stays in
the list without holding back the rest. It runs under the hook's lock, so it starts long work rather than doing it.upload/progress reads, so a
bar moves without a round trip.From an expression. (start js) uploads the files a JavaScript expression gives, read as it runs:
evt.target.files in an input's change, after which the input can be cleared ("; evt.target.value = ''"), or
evt.dataTransfer.files in a drop.
From a drop area. (upload/drop-area attrs start) makes an element a drop area for start: files dragged over
it are its own, and a drop uploads them. The innermost area under the pointer takes a drag, so an area can hold
another, or be the whole page, and a drag of text or a link passes. (upload/dropping start) is an expression, true
while files are over the area, for the app's own overlay:
[:div (upload/drop-area {:class [:relative]} start)
content
[:div (ui/attr {:class [:absolute :inset-0]} :hidden true (str "!" (upload/dropping start)))
"Drop files to add them"]]
Two areas for one start show their overlays together.
Not into the tab. The element the app mounts in (ring/mount) refuses files dropped outside every area, which
the browser would open in the tab; a file input still takes them.
ring/wrap-scripts (Scripts the page needs).connect-src must allow it.Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |