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.
- 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.
- 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.
- handle the upload before creating user resulted in a redirect to the create page before the user is saved.
- 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.