In my personal project — an RSS reader — I wanted to add a feature that lets users upload an OPML file containing their RSS subscriptions. (The OPML parsing itself is covered in Counting Successful Records in Elixir with Enum.)

Today, I’ll walk through how I added the file upload functionality using Phoenix LiveView.

Setting up the upload in mount/3

First, I updated the mount/3 callback to allow file uploads:

defmodule RssReaderWeb.FolderLive.Show do
  use RssReaderWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    {:ok,
     socket
     |> allow_upload(:file,
       accept: :any,
       max_entries: 1,
       max_file_size: 10_000_000,
       auto_upload: true,
       progress: &handle_progress/3
     )}
  end
end

This configuration allows a single file up to 10 MB to be uploaded automatically, and sets up a handle_progress/3 callback to process the upload.

Handling upload progress

Next, I implemented the handle_progress/3 function, along with a basic handle_event/3 for validation:

defmodule RssReaderWeb.FolderLive.Show do
  alias RssReader.Feeds.OpmlImport

  @impl true
  def handle_event("validate", _params, socket) do
    {:noreply, socket}
  end

  defp handle_progress(:file, entry, socket) do
    if entry.done? do
      uploaded_files =
        consume_uploaded_entries(socket, :file, fn %{path: path}, _entry ->
          {:ok, File.read!(path)}
        end)

      file_content = List.first(uploaded_files)

      folder_id = socket.assigns.folder.id

      source_count = OpmlImport.import(file_content, folder_id)

      {sources, unread_count} = sources_with_unread_count(folder_id)

      {:noreply,
       socket
       |> assign(:sources, sources)
       |> assign(:unread_count, unread_count)
       |> put_flash(:info, "#{source_count} sources imported!")}
    else
      {:noreply, socket}
    end
  end
end

Here’s what’s happening:

  • Once the upload is complete (entry.done?), the file is consumed and its content is read.
  • I then call OpmlImport.import/2 to import the subscriptions into the current folder.
  • After importing, I refresh the folder’s sources and unread count.
  • Finally, I update the LiveView state and show a success flash with the number of sources imported.

Adding the upload button to the template

Finally, I added a simple file upload button in the HEEx template:

<form phx-submit="upload">
  <.live_file_input upload={@uploads.file} class="hidden" />
  <label
    for={@uploads.file.ref}
    class="inline-block cursor-pointer rounded bg-white px-2 py-1 text-xs font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-100"
  >
    <.icon name="hero-arrow-up-on-square-stack" size="4" />
  </label>
</form>

The <.live_file_input> is hidden, and the <label> acts as a styled button. Clicking the label triggers the file selector.