HTML + Anvil File Uploader

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_types, 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:

anvil uploader gif

Happy building!

8 Likes

Thanks for sharing!!!

1 Like

Pleasure, hope you find a use for it!

1 Like