How to Do Live Uploads in Phoenix LiveView
Tags: elixir
Published: 2021-10-19 14:19:21

The LiveView framework supports all of the most common features that Single-Page Apps must offer their users, including multipart uploads. In fact, LiveView can give us highly interactive file uploads, right out of the box.

In this post, we'll add a file upload feature to an existing Phoenix LiveView application. Along the way, you'll learn how to use LiveView to display upload progress and feedback while editing and saving uploaded files.

## Setting Up the Image Upload Feature
*The live upload examples that we'll be looking at in this post are drawn from the "Forms and Changesets" chapter in my book, [Programming LiveView](https://pragprog.com/titles/liveview/programming-phoenix-liveview/), co-authored with Bruce Tate. Check it out for an even deeper dive into LiveView forms and so much more.*

In this example, we have an online game store — Arcade — that allows users to browse and review products. Admins can access a product management interface to create, edit, and delete the products we offer our users. We'll give admins the ability to upload a product image that is then stored in the database along with the given product. Let's plan out our new image upload feature before we start writing any code.

We'll begin in our application core by adding an `image_upload` field to the table and schema for products. Then, we'll create a LiveView form component that supports file uploads. Finally, we'll teach our component to report on upload progress and other bits of upload feedback.

This post will focus on adding the live upload functionality to an existing LiveView app that already implements:

- A live view for displaying products.
- A LiveView component that contains a form for creating/editing products.

We'll zero in on the code required to add the upload functionality to this form.

[Check out my earlier post for a basic introduction to working with forms in LiveView](https://blog.appsignal.com/2021/09/28/real-time-form-validations-with-phoenix-liveview.html).

Now we're ready to write some code.

## Persist Product Images in Phoenix LiveView

Assuming our Phoenix LiveView app already has a `products` table and `Product` schema, we'll now update both to store an `image_upload` attribute. This attribute will point to the location of the uploaded file. Once we have our backend wired up, we'll be able to update the existing live view form to accommodate file uploads for a product.

We'll start at the database layer by generating a migration to add a field, `:image_upload`, to the `products` table.

First, generate your migration file:

```bash
[arcade] ➔ mix ecto.gen.migration add_image_to_products
* creating priv/repo/migrations/20201231152152_add_image_to_products.exs
```

This creates a migration file for us, `priv/repo/migrations/20201231152152_add_image_to_products.exs`. Open up that file and key in the contents to the `change` function:

```elixir
defmodule Arcade.Repo.Migrations.AddImageToProducts do
  use Ecto.Migration

  def change do
    alter table(:products) do
      add :image_upload, :string
    end
  end
end
```

This code will add the new database field when we run the migration. Let's do that now:

```bash
[arcade] ➔ mix ecto.migrate

[info]  == Running 20201231152152 \

Arcade.Repo.Migrations.AddImageToProducts.change/0 forward
10:22:24.034 [info]  alter table products

10:22:24.041 [info]  == Migrated 20201231152152 in 0.0s
```

This migration added a new column — `:image_upload` — of type `:string` to the `products` table, but our schema still needs attention.

Update the corresponding `Product` schema by adding the new `:image_upload` field to the `schema` function, which should look like this:

```elixir
defmodule Arcade.Catalog.Product do
  use Ecto.Schema
  import Ecto.Changeset

  schema "products" do
    field :description, :string
    field :name, :string
    field :sku, :integer
    field :unit_price, :float
    field :image_upload, :string
    timestamps()
  end

  @doc false
  def changeset(product, attrs) do
    product
    |> cast(attrs, [:name, :description, :unit_price, :sku, :image_upload])
    |> validate_required([:name, :description, :unit_price, :sku])
    |> validate_number(:unit_price, greater_than: 0)
    |> unique_constraint(:sku)
  end
end
```

Remember, the changeset `cast/4` function must explicitly whitelist new fields, so make sure you add the `:image_upload` attribute there, as shown above.

Now that the changeset has an `:image_upload` attribute, we can save product records that know their image upload location. With that in place, we can make an image upload field available in the `ProductLive.FormComponent`'s form. We're one step closer to giving users the ability to save products with images.

Now, let's turn our attention to the component.

## How to Allow Live Uploads

We'll see our product changeset in action in a bit. First, we need to give the product form the ability to support file uploads. In our Phoenix application, both the "new product" and "edit product" pages use the `ProductLive.FormComponent`. This provides one centralized place to maintain our product form. Changes to this component will enable users to upload an image for a new product and a product they are editing.

To enable uploads for our component, or any live view, we need to call the `allow_upload/3` function with a first argument of the socket. This will put the data into socket assigns that the LiveView framework will then use to perform file uploads. So, for a component, we'll call `allow_upload/3` when the component first starts up and establishes its initial state in the `update/2` function. For a live view, we'd call `allow_upload/3` in the `mount/3` function.

The `allow_upload/3` function is a reducer that takes in an argument of the socket, the upload name, and the upload options and returns an annotated socket. Supported options include file types, file size, number of files per upload name, and more. Let's see it in action:

```elixir
defmodule ArcadeWeb.ProductLive.FormComponent do
  use ArcadeWeb, :live_component
  alias Arcade.Product

  @impl true
  def update(%{product: product} = assigns, socket) do
    changeset = Product.changeset(product, %{})
    {:ok, socket
      |> assign(assigns)
      |> assign(:changeset, changeset)
      |> allow_upload(:image, accept: ~w(.jpg .jpeg .png), max_entries: 1)}
  end
```

In `allow_upload/3`, we pipe in a socket and specify a name for our upload: `:image`. We also provide some options — the maximum number of permitted files and the accepted file formats.

Let's take a look at what our socket assigns looks like after `allow_upload/3` is invoked:


```elixir
%{
  # ...
  uploads: %{
    __phoenix_refs_to_names__: %{"phx-FlZ_j-hPIdCQuQGG" => :image},
    image: #Phoenix.LiveView.UploadConfig<
      accept: ".jpg,.jpeg,.png",
      entries: [],
      errors: [],
      max_entries: 1,
      max_file_size: 8000000,
      name: :image,
      progress_event: #Function<1.71870957/3 ...>,
      ref: "phx-FlZ_j-hPIdCQuQGG",
      ...
    >
  }
}
```

The socket now contains an `:uploads` map that specifies the configuration for each upload field your live view allows. We allowed uploads for an upload called `:image`. So our map contains a key of `:image` pointing to a value of the configuration constructed using the options we gave `allow_upload/3`. This means that we can add a file upload field called `:image` to our form, and LiveView will track the progress of files uploaded via the field within `socket.assigns.uploads.image`.

You can call `allow_upload/3` multiple times with different upload names, thus allowing any number of file uploads in a given live view or component. For example, you could have a form that allows a user to upload a main image, a thumbnail image, a hero image, and more.

Now that we've set up our uploads state, let's take a closer look at the `:image` upload configuration.

### Upload Configurations in Phoenix LiveView

The `:image` upload config looks something like this:


```elixir
#Phoenix.LiveView.UploadConfig<
  accept: ".jpg,.jpeg,.png",
  entries: [],
  errors: [],
  max_entries: 1,
  max_file_size: 8000000,
  name: :image,
  progress_event: #Function<1.71870957/3 ...>,
  ref: "phx-FlZ_j-hPIdCQuQGG",
  ...
>
```

Notice that it contains the configuration options we passed to `allow_upload/3`: the accepted file types list and file formats.

It also has an attribute called `:entries`, which points to an empty list. When a user uploads a file for the `:image` form field, LiveView will automatically update this list with the file upload entry as it completes.

Similarly, the `:errors` list starts out empty and is automatically populated by LiveView with any errors from an invalid file upload entry.

In this way, the LiveView framework does the work of performing the file upload and tracking its state for you. We'll see both of these attributes in action in a bit.

Now that we've allowed uploads in our component, we're ready to update the template with the file upload form field.

## Render the File Upload Field

You'll use the `Phoenix.LiveView.Helpers.live_file_input/2` function to generate the HTML for a file upload form field. Here's a look at our form component template:

```elixir
<%= f = form_for @changeset, "#",
          id: "product-form",
          phx_target: @myself,
          phx_change: "validate",
          phx_submit: "save" %>

  <%= label f, :name %>
  <%= text_input f, :name %>
  <%= error_tag f, :name %>

  <%= label f, :description %>
  <%= text_input f, :description %>
  <%= error_tag f, :description %>

  <%= label f, :unit_price %>
  <%= number_input f, :unit_price, step: "any" %>
  <%= error_tag f, :unit_price %>

  <%= label f, :sku %>
  <%= number_input f, :sku %>
  <%= error_tag f, :sku %>

  <% # File upload fields here: %>
  <%= label f, :image %>
  <%= live_file_input @uploads.image %>

  <%= submit "Save", phx_disable_with: "Saving..." %>
</form>
```

Notice the use of `live_file_input/2` with an argument of `@uploads.image`. Remember, `socket.assigns` has a map of uploads. Here, we provide `@uploads.image` to `live_file_input/2` to create a form field with the right configuration and tie that form field to the correct part of socket state. This means that LiveView will update `socket.assigns.uploads.image` with any new entries or errors that occur when a user uploads a file via this form input.

The live view can present upload progress by displaying data from `@uploads.image.entries` and `@uploads.image.errors`. LiveView will handle all of the details of uploading the file and updating socket assigns `@uploads.image` entries and errors for us. All we have to do is render the data that is stored in the socket. We'll take that on soon.

Now, we should be able to see the file upload field displayed in the browser like this:

![file upload field](https://blog.appsignal.com/images/blog/2021-10/file-upload-field.png)

And if you inspect the element, you'll see that the `live_file_input/2` function generated the appropriate HTML:

![file upload button](https://blog.appsignal.com/images/blog/2021-10/file-upload-button.png)

![file upload html](https://blog.appsignal.com/images/blog/2021-10/file-upload-html.png)

You can see that the generated HTML has the `accept=".jpg,.jpeg,.png"` attribute set, thanks to the options we passed to `allow_upload/3`.

LiveView also supports drag-and-drop file uploads. All we have to do is add an element to the page with the `phx-drop-target` attribute, like this:


```elixir
<%= f = form_for @changeset, "#",
          id: "product-form",
          phx_target: @myself,
          phx_change: "validate",
          phx_submit: "save" %>

  # ...

  <div phx-drop-target="<%= @uploads.image.ref %>">
    <%= live_file_input @uploads.image %>
  </div

  # ...
</form>
```

We give the attribute a value of `@uploads.image.ref`. This socket assignment is the ID that LiveView JavaScript uses to identify the file upload form field and tie it to the correct key in `socket.assigns.uploads`. So now, if a user clicks the "choose file" button _or_ drags-and-drops a file into the area of this `div`, LiveView will store the file info in the `socket.assigns.uploads` assignment, under the name of the specified upload, in that upload's `:entries` list.

As with other form interactions, LiveView automatically handles the client/server communication. When the user submits the form, LiveView's JavaScript will first upload the file(s) and then invoke the `handle_event/3` callback for the form's `phx-submit` event. To process the file upload, this event handler will need to consume the file upload stored in `socket.assigns.uploads.image.entries`. Let's do that now.

## Consume Uploaded Entries

Our `handle_event/3` function for the `phx_submit: "save"` form event will use LiveView's [`consume_uploaded_entry/3`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#consume_uploaded_entry/3) function to process the uploaded file. For now, we'll have our function write the uploaded file to our app's static assets in `priv/static/images`. This is so that we can display it on the product show template later on.

Here's our code:

```elixir
defmodule ArcadeWeb.ProductLive.FormComponent do
# ...
  def handle_event("save", product_params, socket) do
    file_path =
      consume_uploaded_entry(socket, :image, fn %{path: path}, _entry ->
        dest = Path.join("priv/static/uploads", Path.basename(path))
        File.cp!(path, dest)
        Routes.static_path(socket, "/uploads/#{Path.basename(dest)}")
      end)

    product = save_product(Map.put(product_params, :image_upload, file_path)

    {:noreply,
      socket
      |> push_redirect(to: Routes.product_show_path(socket, :show, product))}
  end
end
```

We save the image to static assets and return the file path from `consume_uploaded_entry/3`. Then, we call a helper function — `save_product/1` (not pictured here) — to update the product with the given form params, including the new `:image_upload` attribute set to our new file path. Finally, we redirect to the Product Show page.

To see our code in action, let's add some markup to the product show template to display image uploads. Then, we'll try out our feature.

### Display Image Uploads

Open up `lib/arcade_web/live/product_live/show.html.leex` and add the following markup to display the uploaded image or a fallback:

```elixir
<article class="column">
   <img
      alt="product image" width="200" height="200"
      src="<%=Routes.static_path(
              @socket,
              @product.image_upload || "/images/default-thumbnail.jpg")%>">
  </article>
<!-- product details... -->
```


Perfect. Now, we can test drive it. Visit `/products/1/edit` and upload a file:

![edit product image](https://blog.appsignal.com/images/blog/2021-10/edit-product-image.png)

Once you submit the form, you'll see the show page render the newly uploaded image, like this:

![product show image](https://blog.appsignal.com/images/blog/2021-10/product-show-image.png)

We did it! The LiveView framework handled all of the client/server communication details that make the page interactive. LiveView performed the file upload for you and made responding to upload events easy and customizable. You only needed to tell the live view which uploads to track and what to do with uploaded files when the form is submitted. Then you added the file upload form field to the page with the view helper and LiveView handled the rest!

There's one last thing to do. Earlier, I promised *reactive* file uploads that share feedback with the user. Let's take a look at this now.

## Display Upload Feedback in Phoenix LiveView Forms

We know that LiveView automatically updates the `:entries` and `:errors` lists in the uploads config portion of `socket.assigns` once the upload begins. Let's display this information in the template to give the user real-time progress tracking. The code is amazingly simple. We'll iterate over `@uploads.image.entries` to display the progress for each entry:

```elixir
<%= for entry <- @uploads.image.entries do %>
  <p>
    <progress value={entry.progress} max="100"> <%= entry.progress %>% </progress>
  </p>
<% end %>
```

Here, we use the [HTML progress tag](https://www.w3schools.com/tags/tag_progress.asp) to create a simple progress bar that is populated with the progress of our file upload in real-time. As LiveView's JavaScript is uploading the file for you, LiveView is updating the value of the entry's progress in socket assigns. This causes the relevant portion of the template to re-render, thereby showing the updated progress bar in real-time. LiveView handles the work of tracking the changes to the image entry's progress. All we have to do is display it.

You can use a similar approach to iterate over and display any errors stored in `@uploads.image.errors`. You won't have to do any work to validate files and populate errors. LiveView handles those details. All you need to do is display any errors based on the needs of your user interface. Here's a look at the code:

```elixir
<%= for err <- upload_errors(@uploads.image, entry) do %>
  <p class="alert alert-danger"><%= friendly_error(err) %></p>
<% end %>
```

Here, we use the [`Phoenix.LiveView.Helpers.upload_errors/2`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Helpers.html#upload_errors/2) function to return the errors for the specified upload.

The error messages aren't very user-friendly, though. So, we'll implement a helper function, `friendly_error/1`, in our LiveView component that looks like this:

```elixir
defmodule ArcadeWeb.ProductLive.FormComponent do
  # ...

  def error_to_string(:too_large), do: "Image too large"
  def error_to_string(:too_many_files), do: "Too many files"
  def error_to_string(:not_accepted), do: "Unacceptable file type"
end
```

There's more that LiveView file uploads can do. LiveView makes it easy to:

- cancel an upload
- upload multiple files for a given upload config
- upload files directly from the client to a cloud provider

 and more.

 Check out the [LiveView file upload documentation](https://hexdocs.pm/phoenix_live_view/uploads.html#content) for details.

## Wrap-up: Build Complex Forms Easily with Phoenix LiveView

LiveView enables reactive file uploads right out of the box. Without writing any JavaScript, or even any custom HTML, you can build interactive file upload forms directly into your live view.

LiveView handles the details of client/server communication and upload state management, leaving you on the hook to write a very small amount of custom code specifying how your uploads should behave and how uploaded files should be saved.

This is a pattern you'll see again and again in LiveView — the framework handles the communication and state management details of our SPA, and we can focus on writing application-specific code to support our features.

Now that you've had a glimpse of what LiveView can do with form uploads, you're ready to build complex, interactive forms that support real-time uploads in the wild. Happy coding!

**P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, [subscribe to our Elixir Alchemy newsletter and never miss a single post!](https://blog.appsignal.com/category/elixir-alchemy.html#elixir-alchemy)**

*Sophie is a Senior Engineer at GitHub, co-author of Programming Phoenix LiveView, and co-host of the BeamRad.io podcast. She has a passion for coding education. Historically, she is a cat person but will admit to owning a dog. You can [find her on Twitter](https://twitter.com/sm_debenedetto) or [check out her blog](https://www.thegreatcodeadventure.com/).*