The Anvil file uploader is great, but sometimes you just want a draggy, droppy box, right?
Faced with a need for this very thing I first tried uppy.io (there’s a great tutorial here - and also a useful post on uploading to server rather than S3 here.)
But, Uppy just wasn’t doing it for me, and I also need my app to work disconnected from the interwebs, so some JS served over https:// wouldn’t cut it. That leaves the full DIY option. And it works a charm with both media stored in Data Tables and, also, in a bucket - in my case an implementation of MinIo, hooked up with my app via an uplink.
I thought I would share, because I’m convinced someone else will find this handy at some point.
Let’s build it!
First up, create an empty html form and call it “Uploader.”
Open up the custom html and paste this (change your colours to suit):
<style>
.upload-container {
border: 2px dashed #195073;
border-radius: 10px;
padding: 40px;
text-align: center;
color: #195073;
font-family: sans-serif;
cursor: pointer;
transition: background-color 0.3s;
}
.upload-container:hover {
background-color: #f0f8ff;
}
.upload-container input[type="file"] {
display: none;
}
.browse-link {
color: #195073;
text-decoration: underline;
cursor: pointer;
}
.upload-container.dragover {
background-color: #e6f7ff;
border-color: #195073;
}
.file-list {
list-style: none;
padding-left: 0;
margin-top: 15px;
font-size: 0.9rem;
color: #4A7A8C;
text-align: left;
}
.file-list li {
margin: 2px 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.remove-file {
color: #D4351C;
cursor: pointer;
font-weight: bold;
padding: 0 6px;
user-select: none;
}
.remove-file:hover {
color: #F28627;
}
</style>
<div class="upload-container" id="drop-area">
<input type="file" id="fileElem" multiple accept="*/*">
<label for="fileElem" id="fileLabel">
📁 Drag & Drop files here or <span class="browse-link">browse</span>
</label>
<ul id="fileList" class="file-list"></ul>
</div>
Now get into your form code and drop this in beneath the standard Anvil imports:
import anvil.js
from anvil.js import get_dom_node, window
import base64
class Uploader(UploaderTemplate):
def __init__(self, **properties):
self._file_types = "*/*"
self._multi_file = True
self._selected_files = []
self.init_components(**properties)
self._file_types = "*/*"
self._multi_file = True
self._setup_event_handlers()
self._apply_input_config()
@property
def file_types(self):
return self._file_types
@file_types.setter
def file_types(self, val):
self._file_types = val
self._apply_input_config()
@property
def multi_file(self):
return self._multi_file
@multi_file.setter
def multi_file(self, val):
self._multi_file = val
self._apply_input_config()
def _apply_input_config(self):
file_input = get_dom_node(self).querySelector("#fileElem")
if file_input:
file_input.setAttribute("accept", self._file_types)
if self._multi_file:
file_input.setAttribute("multiple", "true")
else:
file_input.removeAttribute("multiple")
def _setup_event_handlers(self):
dom = get_dom_node(self)
drop_area = dom.querySelector("#drop-area")
file_input = dom.querySelector("#fileElem")
for evt in ["dragenter", "dragover"]:
drop_area.addEventListener(evt, lambda e: (e.preventDefault(), e.stopPropagation(), drop_area.classList.add("dragover")))
for evt in ["dragleave", "drop"]:
drop_area.addEventListener(evt, lambda e: (e.preventDefault(), e.stopPropagation(), drop_area.classList.remove("dragover")))
drop_area.addEventListener("drop", lambda e: self._handle_files(e.dataTransfer.files))
file_input.addEventListener("change", lambda e: self._handle_files(file_input.files))
def _handle_files(self, files):
file_list_el = get_dom_node(self).querySelector("#fileList")
for i in range(files.length):
file = files[i]
# Check for duplicates by file name (optional)
if any(f.name == file.name for f in self._selected_files):
print(f"⚠️ Duplicate skipped: {file.name}")
continue
self._selected_files.append(file)
# Build <li> with filename and ✖ button
li = window.document.createElement("li")
li.innerText = file.name
# Create ✖ span
remove_span = window.document.createElement("span")
remove_span.innerText = "✖"
remove_span.className = "remove-file"
# Bind click event to remove the file
def make_handler(f=file, element=li):
def remove_click(evt):
self._selected_files = [x for x in self._selected_files if x.name != f.name]
element.remove()
return remove_click
remove_span.addEventListener("click", make_handler())
li.appendChild(remove_span)
file_list_el.appendChild(li)
self._process_files(files)
def _process_files(self, files):
uploaded = []
for file in files:
reader = anvil.js.new(window.FileReader)
reader.readAsDataURL(file)
def onloadend(evt, f=file):
if reader.result and ";base64," in reader.result:
b64 = reader.result.split(";base64,")[1]
content = BlobMedia(f.type, base64.b64decode(b64), f.name)
uploaded.append(content)
if len(uploaded) == len(files):
self.raise_event("files_uploaded", files=uploaded)
reader.onloadend = onloadend
def clear(self):
file_input = get_dom_node(self).querySelector("#fileElem")
if file_input:
file_input.value = ""
file_list_el = get_dom_node(self).querySelector("#fileList")
if file_list_el:
file_list_el.innerHTML = ""
self._selected_files = []
Lastly, make the form available as a custom component and add:
-
Property:
file_type
s, string -
Property:
multi-file
, boolean -
Event:
files_uploaded
Now let’s use it!
In the designer, drag your custom component into your form. To keep things tidy add a self.uploader_1.clear()
to your form_show()
.
Use your component properties (designer or code) to set multi_files
to True
or False
, and also to set the kind of file_types
you will accept - or, by all means, leave it free range. (This uses a string so just enter, for example: .jpg,.png,.pdf
)
Then, lastly, set up something to happen when files are uploaded, e.g:
def files_uploaded(self, **event_args):
for f in event_args['files']:
print(f"Uploaded: {f.name}")
anvil.server.call('upload_file', f)'''
And that, Anvilistas, is a bang tidy file uploader in a few minutes, which looks like this:
Happy building!