Live_file_input and form data duplicate redirect

Hello All,

I have a strange problem with the live_file_input.

According to the documentation:
https://hexdocs.pm/phoenix_live_view/uploads.html

you need to have a form with phx-submit=“save”, phx-change=“validate”.

Then you should be able to to use the live_file_input. upon submit it does allow me to process the image and the form data but I have some unexpected behavior.

The way I have it currently setup (which works but not flawlessly)

html.heex:

<div class="mx-auto max-w-6xl border-[1px] border-[#f0f0f0] p-8 rounded-lg shadow-lg shadow-neutral-600">
<.simple_form
  :let={f}
  for={@form}
  image_url={@image_url}
  multipart={true}
  user_id={@user_id}
  phx-submit="save"
  phx-change="validate"
>
  <.error :if={@form.action}>
    Oops, something went wrong! Please check the errors below.
  </.error>

  <:top_actions>
    <.button class="flex">
      <svg
        class="w-6 h-6 me-1"
        xmlns="http://www.w3.org/2000/svg"
        height="48px"
        viewBox="0 -960 960 960"
        width="24px"
        fill="currentColor"
      >
        <path d="M800-663.08v438.46q0 27.62-18.5 46.12Q763-160 735.38-160H224.62q-27.62 0-46.12-18.5Q160-197 160-224.62v-510.76q0-27.62 18.5-46.12Q197-800 224.62-800h438.46L800-663.08ZM760-646 646-760H224.62q-10.77 0-17.7 6.92-6.92 6.93-6.92 17.7v510.76q0 10.77 6.92 17.7 6.93 6.92 17.7 6.92h510.76q10.77 0 17.7-6.92 6.92-6.93 6.92-17.7V-646ZM480-298.46q33.08 0 56.54-23.46T560-378.46q0-33.08-23.46-56.54T480-458.46q-33.08 0-56.54 23.46T400-378.46q0 33.08 23.46 56.54T480-298.46ZM270.77-569.23h296.92v-120H270.77v120ZM200-646v446-560 114Z" />
      </svg>
      Save
    </.button>
    <%= if @user_id > 0 do %>
      <.link
        patch={~p"/users/#{@user_id}"}
        class="phx-submit-loading:opacity-75 flex rounded-lg py-2 px-3 font-semibold leading-6 border border-f0f0f0 hover:bg-amber-600 focus:bg-amber-600"
      >
        <.icon name="hero-no-symbol" class="w-6 h-6 me-1" /> Cancel
      </.link>
    <% else %>
      <.link
        patch={~p"/users/"}
        class="phx-submit-loading:opacity-75 flex rounded-lg py-2 px-3 font-semibold leading-6 border border-f0f0f0 hover:bg-amber-600 focus:bg-amber-600"
      >
        <.icon name="hero-no-symbol" class="w-6 h-6 me-1" /> Cancel
      </.link>
    <% end %>
  </:top_actions>

  <div class="w-full title">User Information</div>
  <div class="flex w-full items-center">
    <div class="w-[40%] flex justify-center flex-wrap">
   
      <%= if @uploads.avatar.entries != [] do %>
        <%= for entry <- @uploads.avatar.entries do %>
          <.live_img_preview entry={entry} class="h-[300px] w-[300px] user-image mt-6" />
        <% end %>
      <% else %>
        <img src={@image_url} class="h-[300px] w-[300px] user-image mt-6" id="" />
      <% end %>
      <label
        class="flex justify-center items-center mt-4 rounded-lg py-2 px-3 font-semibold leading-6 border border-f0f0f0 hover:bg-amber-600 focus:bg-amber-600"
      >
        <.icon name="hero-camera-solid" class="h-5 w-5 me-1" /> Change image
        <.live_file_input upload={@uploads.avatar} class="upload hidden" />
      </label> 
    </div>
    <div class="w-[60%] flex flex-wrap justify-between">
      <.input field={f[:name]} type="text" label="Name" class="w-[49%]" />
      <.input field={f[:email]} type="text" label="Email" class="w-[49%]" />
      <.input field={f[:phone]} type="text" label="Phone" class="w-[49%] mt-4" />
      <.input field={f[:department]} type="text" label="Department" class="w-[49%] mt-4" />
      <.input field={f[:password]} type="password" label="Password" class="w-[49%] mt-4" />
      <.input field={f[:password_confirmation]} type="password" label="Confirm Password" class="w-[49%] mt-4" />
    </div>
  </div>

  <div class="w-full title">User Roles</div>
  <div class="flex w-full flex-wrap justify-between mt-2">
    <.inputs_for :let={item_f} field={f[:user_roles]}>
      <div class="w-[33%] mb-8">
        <.input class="hidden" field={item_f[:role_name]} type="text" />
        <.input class="hidden" field={item_f[:role_id]} type="number" />
        <%!-- i have no idea why the value is different from the one above. but i guess we whave to deal with this wierd shit. --%>
        <.input
          type="checkbox"
          field={item_f[:belongs_in_role]}
          label={item_f[:role_name].value}
          value={item_f.params["belongs_in_role"]}
        />
      </div>
    </.inputs_for>
  </div>

  <:actions>
    <.button class="flex align-center">
      <svg
        class="w-6 h-6 me-1"
        xmlns="http://www.w3.org/2000/svg"
        height="48px"
        viewBox="0 -960 960 960"
        width="24px"
        fill="currentColor"
      >
        <path d="M800-663.08v438.46q0 27.62-18.5 46.12Q763-160 735.38-160H224.62q-27.62 0-46.12-18.5Q160-197 160-224.62v-510.76q0-27.62 18.5-46.12Q197-800 224.62-800h438.46L800-663.08ZM760-646 646-760H224.62q-10.77 0-17.7 6.92-6.92 6.93-6.92 17.7v510.76q0 10.77 6.92 17.7 6.93 6.92 17.7 6.92h510.76q10.77 0 17.7-6.92 6.92-6.93 6.92-17.7V-646ZM480-298.46q33.08 0 56.54-23.46T560-378.46q0-33.08-23.46-56.54T480-458.46q-33.08 0-56.54 23.46T400-378.46q0 33.08 23.46 56.54T480-298.46ZM270.77-569.23h296.92v-120H270.77v120ZM200-646v446-560 114Z" />
      </svg>
      Save
    </.button>
    <%= if @user_id > 0 do %>
      <.link
        patch={~p"/users/#{@user_id}"}
        class="phx-submit-loading:opacity-75 flex align-center rounded-lg py-2 px-3 font-semibold leading-6 border border-f0f0f0 hover:bg-amber-600 focus:bg-amber-600"
      >
        <.icon name="hero-no-symbol" class="w-6 h-6 me-1" /> Cancel
      </.link>
    <% else %>
      <.link
        patch={~p"/users/"}
        class="phx-submit-loading:opacity-75 flex align-center rounded-lg py-2 px-3 font-semibold leading-6 border border-f0f0f0 hover:bg-amber-600 focus:bg-amber-600"
      >
        <.icon name="hero-no-symbol" class="w-6 h-6 me-1" /> Cancel
      </.link>
    <% end %>
  </:actions>
</.simple_form>
</div>

Mount:

def mount(_params, _session, socket) do
    {:ok,
     socket
     |> assign(:page_title, "New User")
     |> assign(:image_url, gravatar_for(%User{}))
     |> assign(:user_id, 0)
     |> assign(:form, UserService.change(%User{}))
     |> assign(:uploaded_files, [])
     |> assign(:auto_upload, true)
     |> allow_upload(:avatar, accept: ~w(.jpg .jpeg .png .gif), max_entries: 1)}
  end

Save:

  def handle_event("save", %{"user" => params}, socket) do
    # Save the user first to ensure unique constrainted is honored.
    case UserService.create_user(params) do
      {:ok, user} ->
        # prepare the socket because redirect seems to do strange stuff when using
        consume_uploaded_entries(socket, :avatar, fn %{path: path}, entry ->
          dest =
            Path.join([
              "priv/static/images/uploads",
              Path.basename(path)
            ])

          File.cp!(path, dest <> Path.extname(entry.client_name))

          UserService.update_user(user, %{
            image_url:
              "/images/uploads/" <> Path.basename(path) <> Path.extname(entry.client_name)
          })

          {:ok, "/images/uploads/" <> Path.basename(path) <> Path.extname(entry.client_name)}
        end)

        # redirects correctly if no image is uploaded.
        # redirects then reloads the page.
        {:noreply,
         socket
         |> put_flash(:info, "User " <> user.name <> " is successfully created.")
         |> push_navigate(to: ~p"/users/#{user.id}")}

      {:error, errors} ->
        {:noreply, assign(socket, :form, errors)}
    end
  end

The problem I am facing is a bit confusing.

  1. If no image is uploaded the socket will do everything in order, 1. save user, 2. checks if any images need processing. 3. redirects to the created user.
  2. but when an image is uploaded it does the following 1. saves the users, 2. handles the upload. 3. redirects to the created users, 4. redirects to the created user with OK200.

here is a log that I have for both scenarios in the hopes that it helps
for no image:

"redirect"
[debug] Replied in 189ms
[debug] MOUNT SigportalWeb.UsersLive.Show
  Parameters: %{"id" => "41"}
  Session: %{"_csrf_token" => "TP6Rtk_158bj7fIYoJE7pVwj", "live_socket_id" => "users_sessions:684EKzRvOJi_jZEmR-9oQCObOYXgS8BiXOd_T_QEaIY=", "user_token" => <<235, 206, 4, 43, 52, 111, 56, 152, 191, 141, 145, 38, 71, 239, 104, 64, 35, 155, 57, 133, 224, 75, 192, 98, 92, 231, 127, 79, 244, 4, 104, 134>>}
[debug] QUERY OK source="users_tokens" db=0.2ms idle=1554.7ms
SELECT u1."id", u1."email", u1."hashed_password", u1."confirmed_at", u1."image_url", u1."name", u1."super_user", u1."phone", u1."department", u1."inserted_at", u1."updated_at" FROM "users_tokens" AS u0 INNER JOIN "users" AS u1 ON u1."id" = u0."user_id" WHERE ((u0."token" = $1) AND (u0."context" = $2)) AND (u0."inserted_at" > $3::timestamp + (-(60)::numeric * interval '1 day')) [<<235, 206, 4, 43, 52, 111, 56, 152, 191, 141, 145, 38, 71, 239, 104, 64, 35, 155, 57, 133, 224, 75, 192, 98, 92, 231, 127, 79, 244, 4, 104, 134>>, "session", ~U[2024-08-15 03:12:47.527859Z]]
↳ Phoenix.LiveView.Utils.assign_new/3, at: lib/phoenix_live_view/utils.ex:79
[debug] Replied in 605µs
[debug] HANDLE PARAMS in SigportalWeb.UsersLive.Show
  Parameters: %{"id" => "41"}
[debug] QUERY OK source="users" db=0.1ms idle=1555.2ms
SELECT u0."id", u0."email", u0."hashed_password", u0."confirmed_at", u0."image_url", u0."name", u0."super_user", u0."phone", u0."department", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [41]
↳ Sigportal.Users.UserService.get!/1, at: lib/sigportal/users/usersservice.ex:61
[debug] QUERY OK source="userrole" db=0.2ms idle=1555.5ms
SELECT u0."id", u0."role_id", u0."user_id", u0."belongs_in_role", u0."role_name", u0."user_id" FROM "userrole" AS u0 WHERE (u0."user_id" = $1) ORDER BY u0."user_id" [41]
↳ SigportalWeb.UsersLive.Show.handle_params/3, at: lib/sigportal_web/live/settings/user/user_live/show.ex:17
[debug] Replied in 690µs

With an image:

"redirect"
[debug] Replied in 190ms
[debug] MOUNT SigportalWeb.UsersLive.Show
  Parameters: %{"id" => "43"}
  Session: %{"_csrf_token" => "TP6Rtk_158bj7fIYoJE7pVwj", "live_socket_id" => "users_sessions:684EKzRvOJi_jZEmR-9oQCObOYXgS8BiXOd_T_QEaIY=", "user_token" => <<235, 206, 4, 43, 52, 111, 56, 152, 191, 141, 145, 38, 71, 239, 104, 64, 35, 155, 57, 133, 224, 75, 192, 98, 92, 231, 127, 79, 244, 4, 104, 134>>}
[debug] QUERY OK source="users_tokens" db=0.3ms idle=1276.1ms
SELECT u1."id", u1."email", u1."hashed_password", u1."confirmed_at", u1."image_url", u1."name", u1."super_user", u1."phone", u1."department", u1."inserted_at", u1."updated_at" FROM "users_tokens" AS u0 INNER JOIN "users" AS u1 ON u1."id" = u0."user_id" WHERE ((u0."token" = $1) AND (u0."context" = $2)) AND (u0."inserted_at" > $3::timestamp + (-(60)::numeric * interval '1 day')) [<<235, 206, 4, 43, 52, 111, 56, 152, 191, 141, 145, 38, 71, 239, 104, 64, 35, 155, 57, 133, 224, 75, 192, 98, 92, 231, 127, 79, 244, 4, 104, 134>>, "session", ~U[2024-08-15 03:13:31.249277Z]]
↳ Phoenix.LiveView.Utils.assign_new/3, at: lib/phoenix_live_view/utils.ex:79
[debug] Replied in 729µs
[debug] HANDLE PARAMS in SigportalWeb.UsersLive.Show
  Parameters: %{"id" => "43"}
[debug] QUERY OK source="users" db=0.2ms idle=1276.7ms
SELECT u0."id", u0."email", u0."hashed_password", u0."confirmed_at", u0."image_url", u0."name", u0."super_user", u0."phone", u0."department", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [43]
↳ Sigportal.Users.UserService.get!/1, at: lib/sigportal/users/usersservice.ex:61
[debug] QUERY OK source="userrole" db=0.1ms idle=1277.0ms
SELECT u0."id", u0."role_id", u0."user_id", u0."belongs_in_role", u0."role_name", u0."user_id" FROM "userrole" AS u0 WHERE (u0."user_id" = $1) ORDER BY u0."user_id" [43]
↳ SigportalWeb.UsersLive.Show.handle_params/3, at: lib/sigportal_web/live/settings/user/user_live/show.ex:17
[debug] Replied in 798µs
[info] GET /users/43
[debug] Processing with SigportalWeb.UsersLive.Show.show/2
  Parameters: %{"id" => "43"}
  Pipelines: [:browser, :require_authenticated_user]
[debug] QUERY OK source="users_tokens" db=0.1ms idle=470.5ms
SELECT u1."id", u1."email", u1."hashed_password", u1."confirmed_at", u1."image_url", u1."name", u1."super_user", u1."phone", u1."department", u1."inserted_at", u1."updated_at" FROM "users_tokens" AS u0 INNER JOIN "users" AS u1 ON u1."id" = u0."user_id" WHERE ((u0."token" = $1) AND (u0."context" = $2)) AND (u0."inserted_at" > $3::timestamp + (-(60)::numeric * interval '1 day')) [<<235, 206, 4, 43, 52, 111, 56, 152, 191, 141, 145, 38, 71, 239, 104, 64, 35, 155, 57, 133, 224, 75, 192, 98, 92, 231, 127, 79, 244, 4, 104, 134>>, "session", ~U[2024-08-15 03:13:31.441969Z]]
↳ SigportalWeb.UserAuth.fetch_current_user/2, at: lib/sigportal_web/user_auth.ex:140
[debug] QUERY OK source="role" db=0.2ms idle=470.4ms
SELECT r0."id", r0."name", r0."description" FROM "role" AS r0 INNER JOIN "userrole" AS u1 ON u1."role_id" = r0."id" WHERE ((u1."user_id" = $1) AND (u1."belongs_in_role" = TRUE)) [1]
↳ Sigportal.UserRoleService.fetch_roles_users_belongs_too/1, at: lib/sigportal/roles/user_roles/user_roleservice.ex:19
[debug] QUERY OK source="rolepermission" db=0.2ms idle=470.7ms
SELECT r0."id", r0."role_id", r0."permission", r0."sitemap_code", r0."sitemap_name", r0."sitemap_level", r0."sitemap_parent", r0."sitemap_url", r0."role_id" FROM "rolepermission" AS r0 WHERE (r0."role_id" = $1) ORDER BY r0."role_id" [1]
↳ SigportalWeb.UserAuth.fetch_user_permissions/1, at: lib/sigportal_web/user_auth.ex:42
[debug] QUERY OK source="users" db=0.1ms idle=209.9ms
SELECT u0."id", u0."email", u0."hashed_password", u0."confirmed_at", u0."image_url", u0."name", u0."super_user", u0."phone", u0."department", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [43]
↳ Sigportal.Users.UserService.get!/1, at: lib/sigportal/users/usersservice.ex:61
[debug] QUERY OK source="userrole" db=0.1ms idle=208.9ms
SELECT u0."id", u0."role_id", u0."user_id", u0."belongs_in_role", u0."role_name", u0."user_id" FROM "userrole" AS u0 WHERE (u0."user_id" = $1) ORDER BY u0."user_id" [43]
↳ SigportalWeb.UsersLive.Show.handle_params/3, at: lib/sigportal_web/live/settings/user/user_live/show.ex:17
[info] Sent 200 in 5ms
[info] CONNECTED TO Phoenix.LiveView.Socket in 51µs
  Transport: :websocket
  Serializer: Phoenix.Socket.V2.JSONSerializer
  Parameters: %{"_csrf_token" => "HmdbMUAIcllkCBY7ZRcPLh4SdnY-OyEEJ7mc4c-hQ0tQRqFwqX3ANmVn", "_live_referer" => "undefined", "_mounts" => "0", "_track_static" => %{"0" => "http://localhost:4000/assets/app.css", "1" => "http://localhost:4000/assets/app.js"}, "vsn" => "2.0.0"}
[debug] MOUNT SigportalWeb.UsersLive.Show
  Parameters: %{"id" => "43"}
  Session: %{"_csrf_token" => "TP6Rtk_158bj7fIYoJE7pVwj", "live_socket_id" => "users_sessions:684EKzRvOJi_jZEmR-9oQCObOYXgS8BiXOd_T_QEaIY=", "user_token" => <<235, 206, 4, 43, 52, 111, 56, 152, 191, 141, 145, 38, 71, 239, 104, 64, 35, 155, 57, 133, 224, 75, 192, 98, 92, 231, 127, 79, 244, 4, 104, 134>>}
[debug] QUERY OK source="users_tokens" db=0.2ms idle=626.4ms
SELECT u1."id", u1."email", u1."hashed_password", u1."confirmed_at", u1."image_url", u1."name", u1."super_user", u1."phone", u1."department", u1."inserted_at", u1."updated_at" FROM "users_tokens" AS u0 INNER JOIN "users" AS u1 ON u1."id" = u0."user_id" WHERE ((u0."token" = $1) AND (u0."context" = $2)) AND (u0."inserted_at" > $3::timestamp + (-(60)::numeric * interval '1 day')) [<<235, 206, 4, 43, 52, 111, 56, 152, 191, 141, 145, 38, 71, 239, 104, 64, 35, 155, 57, 133, 224, 75, 192, 98, 92, 231, 127, 79, 244, 4, 104, 134>>, "session", ~U[2024-08-15 03:13:31.874111Z]]
↳ Phoenix.LiveView.Utils.assign_new/3, at: lib/phoenix_live_view/utils.ex:79
[debug] Replied in 7ms
[debug] HANDLE PARAMS in SigportalWeb.UsersLive.Show
  Parameters: %{"id" => "43"}
[debug] QUERY OK source="users" db=0.1ms idle=626.4ms
SELECT u0."id", u0."email", u0."hashed_password", u0."confirmed_at", u0."image_url", u0."name", u0."super_user", u0."phone", u0."department", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [43]
↳ Sigportal.Users.UserService.get!/1, at: lib/sigportal/users/usersservice.ex:61
[debug] QUERY OK source="userrole" db=0.1ms idle=626.3ms
SELECT u0."id", u0."role_id", u0."user_id", u0."belongs_in_role", u0."role_name", u0."user_id" FROM "userrole" AS u0 WHERE (u0."user_id" = $1) ORDER BY u0."user_id" [43]
↳ SigportalWeb.UsersLive.Show.handle_params/3, at: lib/sigportal_web/live/settings/user/user_live/show.ex:17
[debug] Replied in 621µs

I have tried many approaches.

  1. handle the upload before creating user resulted in a redirect to the create page before the user is saved.
  2. putting handle upload in a separate function to call and only on {ok, somevalue} do the create.

I would appreciate any help or explanation on how to resolve this issue as it should not redirect for a second time.

Is it the use of push_navigate/2 that’s causing your issue (Phoenix.LiveView — Phoenix LiveView v0.20.17)

This navigates to another LiveView, shutting the existing one down - which might explain the behaviour you’re seeing.

Perhaps taking a look at push_patch/2 (Phoenix.LiveView — Phoenix LiveView v0.20.17)

I hope I’ve understood your issue correctly.

Hello @muelthe

It seems when I use push_patch I get an error:

[error] GenServer #PID<0.964.0> terminating
** (ArgumentError) cannot push_patch/2 to %URI{scheme: "http", userinfo: nil, host: "localhost", port: 4000, path: "/users/22", query: nil, fragment: nil}

The image uploads correctly my users is created correctly, it just does not redirect correctly.

the push_navigate works properly if there is no image present saving the user and then redirecting with the proper flash.

With an image it also saves correctly redirects with the flash message but then it reloads itself saying “getting you back on track”

I have a video but I cannot seem to upload it to show you.

That message seems to me that your LiveView crashes and reloads

Of course, my mistake.

I’ve noted a couple of things, that may be worth looking at.

Having not used the uploads feature, I’m not certain of the outcome, but you’re not updating the socket once you’ve consumed the file e.g. {:no_reply, update(socket, :uploaded_files, ...} as per the example.

Secondly, you’re writing to "priv/static/images/uploads", which depending on your setup might be being picked up by code_reloader and hence the page reload when you write a file to that directory - check the entry here in the example for details: Uploads — Phoenix LiveView v0.20.17

Hello @muelthe

I think your second point turns out to be correct. it seems that the code_reloader checks that folder for changes and reloads the liveview.

Once I changed to "priv/static/uploads the error went away.

Thank you for your answer this had me stumped for a few days.

1 Like