7
votes

I have a form in a Phoenix LiveView that contains a file_input. I want to use it to allow a user to upload an image. I'm having trouble understanding what the form is sending to my backend, and what I can do with it. I expected a %Plug.Upload{} representation of the image file, as described in documentation, but instead I just get "[object File]".

Note that I am not backing the form with a changeset, because I am not using Ecto:

<%= f = form_for :post, "#", phx_submit: :create_post, phx_change: :image_attach, multipart: true %>
  <%= hidden_input f, :user_id, value: @current_user.account_id %>
  <%= textarea f, :message, class: "social-post-box", placeholder: "Something on your mind?" %>
  <div class="post-submit-container">
    <%= submit "Post", class: "post-submit" %>
    <label for="post_image" class="post-submit-image"></label
    <%= file_input f, :image %      
  </div>
</form>

I have a handler in the LiveView module to handle the submitted form, and when I inspect the image upload I see "[object File]"

def handle_event("create_post", %{"post" => post_params}, socket) do
  IO.inspect post_params["image"]
  {:noreply, socket}
end

I tried prying at this location so that I could run i post_params["image"], and it explains that the object is a bitstring, ie just a binary. So it's literally just the text "[object File]", and not even a file at all?

What is it that I am receiving from my form? Why isn't it a %Plug.Upload{}? How can I achieve my goal of saving this image upload to the local filesystem?

1
AFAIK, file uploading is not officially supported yet. See the Github issue you can find some workarounds as well in the commentssbacarob
You might also find this helpfulsbacarob

1 Answers

11
votes

As @sbacaro pointed out, file uploads are not yet supported in LiveView forms.

More info on this:

I implemented a Javascript workaround to manually send the form without refreshing the page (so that other parts of the LiveView an continue to function normally).

But were also issues with the way Phoenix handled CSRF tokens in LiveViews. It turns out the LiveView creates a new token when the socket connects from the client, and this token won't be recognized by controllers listening to POSTs from the form. To workaround this you need to manually pass the token into the LiveView.

Overall, this workaround works fine, but I hope that someday in the future someone will point out here that file uploads have achieved support in LiveViews and share an easier way.

My form now looks like this. Note the manual specification of the csrf token:

<%= f = form_for :post, Routes.profile_path(UdsWeb.Endpoint, :post_social, @current_user.username), [phx_change: :image_attach, multipart: true, id: "social-timeline-form", csrf_token: @csrf_token] %>
  <%= hidden_input f, :user_id, value: @current_user.account_id %>
  <%= textarea f, :message, class: "social-post-box", placeholder: "Something on your mind?" %>
  <div class="post-submit-container">
    <%= submit "Post", class: "post-submit" %>
    <label for="post_image" class="post-submit-image"></label>
    <%= file_input f, :image %>
  </div>
</form>

I render the LiveView from within a normal eex template. Note that I'm manually specifying the csrf token here:

<%= Phoenix.LiveView.live_render(@conn, UdsWeb.ProfileTimelineLive, session: %{current_user: @current_user, csrf_token: Phoenix.Controller.get_csrf_token()}, container: {:div, class: "feed"}) %>

The timeline module has a mount function that loads the csrf token into socket assigns:

def mount(%{current_user: current_user, csrf_token: csrf_token}, socket) do
  {:ok, assign(socket, current_user: current_user, csrf_token: csrf_token)}
end

The JS for manually taking control of the form submission isn't really special but here it is:

function handleSocialTimelinePost(e) {
  e.preventDefault();
  let form = document.querySelector("#social-timeline-form");
  let formData = new FormData(form);
  let username = formData.get("post[username]");
  let request = new XMLHttpRequest();
  request.open("POST", `/profile/${username}`);
  request.send(formData);
}

document.querySelector("#social-timeline-form button.post-submit").onclick = handleSocialTimelinePost;