HTML + File Browser

So, the other day I found myself in need of a fancy file uploader with a drag and drop box - which I duly built and shared with the community for fun and frolics here.

But, alas, I then found I needed a File Browser, because slinging things around repeating panels and dynamically adding and removing menu links wasn’t cutting it in the app design.

It wasn’t as painful as you might think (I’ll reserve that for creating an anvil native, fully sovereign replacement for S3 - which has been…interesting). So, I thought I would share a little so you can do it too!

I’m going to show you how to build a custom component which allows you get files from your app tables, present them in a neat little UI, and then even drag them around. After that, you can punish yourselves for hours coming up with infernal designs to back it on to.

First, our custom HMTL
Create a new form called FolderBrowserLite and select Custom HTML, then click in to your props to edit the HTML and paste the following:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Simple Folder Browser</title>
    <style>
      body {
        font-family: sans-serif;
        background-color: #f9f9f9;
        padding: 40px;
      }

      .folder-lite {
        font-family: sans-serif;
        font-size: 0.95rem;
        color: #195073;
        padding: 20px;
      }

      .folder-lite .section {
        margin-bottom: 15px;
      }

      .folder-lite .folder-header {
        font-weight: bold;
        padding: 6px;
        border-radius: 6px;
        background-color: #f5faff;
        cursor: pointer;
        user-select: none;
        transition: background 0.2s;
      }

      .folder-lite .folder-header:hover {
        background-color: #e0f0ff;
      }

      .folder-lite .file-list {
        margin-left: 20px;
        display: none;
      }

      .folder-lite .file-item {
        padding: 4px 0;
        cursor: pointer;
      }

      .folder-lite .file-item:hover {
        text-decoration: underline;
      }

      .folder-lite .file-item::before {
        content: "📄 ";
      }
    </style>
  </head>
  <body>

    <h2>📁 In-app Files</h2>

    <div class="folder-lite" id="liteRoot">
      <!-- Dynamic content will be injected here -->
    </div>

  </body>
</html>

It doesn’t look like much, but you should see:
image

Now for the Form Code
In your form code, below the stock imports, paste in this:

from anvil.js import get_dom_node, window
from anvil.designer import in_designer
import json

class FolderBrowserLite(FolderBrowserLiteTemplate):
  def __init__(self, **properties):
    self.click_event = None
    self.init_components(**properties)
    if not in_designer:
      self.render_folders()

  def render_folders(self):
    dom = get_dom_node(self)
    root = dom.querySelector("#liteRoot")
    root.innerHTML = ""

    my_files = list(anvil.server.call("get_my_files"))
    shared_files = list(anvil.server.call("get_shared_files"))

    anything_to_show = False
    self.sections = {}

    if my_files:
      anything_to_show = True
      my_section = self._render_section("My Files", my_files)
      root.appendChild(my_section)
      self.sections["My Files"] = my_section.querySelector(".file-list")

    if shared_files:
      anything_to_show = True
      shared_section = self._render_section("Shared", shared_files)
      root.appendChild(shared_section)
      self.sections["Shared"] = shared_section.querySelector(".file-list")

    if not anything_to_show:
      msg = window.document.createElement("div")
      msg.style.marginTop = "10px"
      msg.style.fontStyle = "italic"
      msg.style.color = "#888"
      msg.innerText = "📂 Nothing to see here yet. Try uploading or sharing a file."
      root.appendChild(msg)

  def _render_section(self, title, file_rows):
    section = window.document.createElement("div")
    section.className = "section"

    header = window.document.createElement("div")
    header.className = "folder-header"
    header.innerText = title

    file_list = window.document.createElement("div")
    file_list.className = "file-list"
    file_list.style.display = "block"

    # Enable drag-drop events
    file_list.addEventListener("dragover", lambda e: e.preventDefault())
    file_list.addEventListener("dragenter", lambda e, el=file_list: el.classList.add("drop-target"))
    file_list.addEventListener("dragleave", lambda e, el=file_list: el.classList.remove("drop-target"))
    file_list.addEventListener("drop", lambda e, target=title, el=file_list: self._handle_drop(e, target, el))

    for row in file_rows:
      file_el = window.document.createElement("div")
      file_el.className = "file-item"
      file_el.innerText = row["filename"]
      file_el.setAttribute("draggable", "true")
      file_el.dataset["fileName"] = row["filename"]
      file_el.dataset["currentSection"] = title

      def handle_click(evt, r=row):
        self.raise_event("file_click", file=r)

      file_el.addEventListener("click", handle_click)
      file_el.addEventListener("dragstart", lambda evt, r=file_el: evt.dataTransfer.setData("text/plain", json.dumps({
        "file_name": r.dataset["fileName"],
        "from_section": r.dataset["currentSection"]
      })))

      file_list.appendChild(file_el)

    section.appendChild(header)
    section.appendChild(file_list)
    return section

  def _handle_drop(self, event, to_section, container):
    event.preventDefault()
    container.classList.remove("drop-target")

    try:
      data = json.loads(event.dataTransfer.getData("text/plain"))
      file_name = data.get("file_name")
      from_section = data.get("from_section")

      if not file_name or not from_section or from_section == to_section:
        return

      # Safety: Validate filename
      if any(c in file_name for c in ['..', '/', '\\']):
        raise ValueError("Invalid filename.")

      current_share = (to_section == "My Files")  # True means private
      anvil.server.call("update_sharing", file_name, current_share)

      # Remove from old UI
      old_list = self.sections.get(from_section)
      for child in list(old_list.children):
        if child.innerText == file_name:
          child.remove()

      # Add to new UI
      self._add_file_to_section(container, file_name, to_section)

    except Exception as e:
      alert(f"⚠️ File move failed: {e}")

  def _add_file_to_section(self, container, file_name, section_name):
    el = window.document.createElement("div")
    el.className = "file-item"
    el.innerText = file_name
    el.setAttribute("draggable", "true")
    el.dataset["fileName"] = file_name
    el.dataset["currentSection"] = section_name

    el.addEventListener("click", lambda e: self.raise_event("file_click", file={"filename": file_name}))

    el.addEventListener("dragstart", lambda evt, r=el: evt.dataTransfer.setData("text/plain", json.dumps({
      "file_name": r.dataset["fileName"],
      "from_section": r.dataset["currentSection"]
    })))

    container.appendChild(el)

This handles the dynamic creation of the UI components, drag and drop, and click on file.
After this, set your form as a Custom Component and don’t forget to add the event file_click so you can do something with a file when you click on it.

Now for a table
For the purpose of simplicity, create a new data table called UserFiles with the following columns:

user, linked to row in users table
file,media
filename,text
private,bool

(I’m truncating this for your sanity and mine.)

Now for some server functions

Create a server module and paste this in:

authenticated_call = anvil.server.callable(require_user=True)

@authenticated_call
def update_sharing(file_name: str, private: bool):
  row = app_tables.userfiles.get(user=anvil.users.get_user(), filename=file_name)
  if row:
    row.update(private=private)

def get_my_files():
  return app_tables.userfiles.search(user=anvil.users.get_user(),private=True)

def get_shared_files():
  return app_tables.userfiles.search(user=anvil.users.get_user(),private=False)

Nearly done…

Add your component to a form

Any form will do, start probably easiest if your user is logged in…

Add a click event and just chuck print("Clicked a file...") in there for now.

And, providing you’ve added at least one private file in to your data table you will see:
image

And that, friends and neighbours, is that.

5 Likes