AE
Ash Elixirโ€ข2y ago
axdc

LiveView uploads with AshPhoenix.Form

Hello, I've got a form that allows the user to select from a few example images, or to upload their own and use that. Right now it's a set of radio buttons plus a live_file_input. I've gotten it ~working but it feels pretty dirty and hacky with some showstopping bugs so I'm looking for prior art and best practices when it comes to the Ash Way to do live uploads.
I've searched here and it doesn't look like there's a specific upload extension or type yet, but in the meantime, is there a better or recommended way to do what I'm trying to do? Specifically this:
defp maybe_put_uploaded_wallpaper_url(socket, form) do
wallpaper_url = consume_uploads(socket) |> List.first()

if(wallpaper_url) do
# a wallpaper was uploaded
Logger.info("handling uploaded wallpaper")
form = form |> AshPhoenix.Form.set_data(%{form.data | wallpaper_url: wallpaper_url})
form = %{form | params: %{form.params | "wallpaper_url" => wallpaper_url}}

form = %{
form
| source: %{
form.source
| params: %{form.source.params | "wallpaper_url" => wallpaper_url}
}
}

IO.inspect(form)
else
# pass through form unchanged
form
end
end
defp maybe_put_uploaded_wallpaper_url(socket, form) do
wallpaper_url = consume_uploads(socket) |> List.first()

if(wallpaper_url) do
# a wallpaper was uploaded
Logger.info("handling uploaded wallpaper")
form = form |> AshPhoenix.Form.set_data(%{form.data | wallpaper_url: wallpaper_url})
form = %{form | params: %{form.params | "wallpaper_url" => wallpaper_url}}

form = %{
form
| source: %{
form.source
| params: %{form.source.params | "wallpaper_url" => wallpaper_url}
}
}

IO.inspect(form)
else
# pass through form unchanged
form
end
end
This feels like sin to me. The liveview file upload examples have you patching params but I don't have params, I have an AshPhoenix.Form. One bug example: If I change the image, save, and then change it back to what it originally was, it looks like it persists but actually doesn't. I suspect this may have something to do with how I'm setting the checked attribute? my index.ex: https://gitlab.com/avoh-labs/panacea/-/blob/main/lib/panacea_web/live/rosegarden/configuration_live/index.ex my index.html.heex: https://gitlab.com/avoh-labs/panacea/-/blob/main/lib/panacea_web/live/rosegarden/configuration_live/index.html.heex#L597 Thank you. Posting here is always clarifying. chasing down some more threads now. prepare_source maybe
7 Replies
ZachDaniel
ZachDanielโ€ข2y ago
๐Ÿ‘‹ So I haven't actually set this up yet, but I think what you want is AshPhoenix.Form.update_form ๐Ÿ™‚ actually nvm, this isn't nested Yeah, so prepare_source may actually be your best bet. That or basically hand-rolling this
# returns a socket instead of your original form example
defp maybe_put_uploaded_wallpaper_url(socket, form) do
wallpaper_url = consume_uploads(socket) |> List.first()

if(wallpaper_url) do
assign(socket, :wallpaper_url, wallpaper_url)
else
socket
end
end
# returns a socket instead of your original form example
defp maybe_put_uploaded_wallpaper_url(socket, form) do
wallpaper_url = consume_uploads(socket) |> List.first()

if(wallpaper_url) do
assign(socket, :wallpaper_url, wallpaper_url)
else
socket
end
end
Hand rolling it looks like this and then when its time to submit you'd do submit(..., params: Map.put(params, "wallpaper_url", socket.assigns.wallpaper_url)) but you could also do:
defp maybe_put_uploaded_wallpaper_url(socket, form) do
wallpaper_url = consume_uploads(socket) |> List.first()

if(wallpaper_url) do
AshPhoenix.Form.prepare_source(form, fn changeset ->
Ash.Changeset.change_attribute(changeset, :wallpaper_url, wallpaper)url)
end)
else
form
end
end
defp maybe_put_uploaded_wallpaper_url(socket, form) do
wallpaper_url = consume_uploads(socket) |> List.first()

if(wallpaper_url) do
AshPhoenix.Form.prepare_source(form, fn changeset ->
Ash.Changeset.change_attribute(changeset, :wallpaper_url, wallpaper)url)
end)
else
form
end
end
the first one is probably better in this case (or perhaps a combination of the two) so that you can use that wallpaper_url assign to manage the UI state (i.e the upload is complete and this is the url)
axdc
axdcOPโ€ข2y ago
EDIT: I'm getting ** (KeyError) key :wallpaper_url not found in: %{__changed__: %{}, ... when I don't change the wallpaper, so I think I might have to assign that to something at mount? But when I upload it persists now, fixed a bug with using an atom instead of a string as a key
def handle_event("save", %{"form" => params}, socket) do
form = AshPhoenix.Form.validate(socket.assigns.form, params)

socket = socket |> maybe_put_uploaded_wallpaper_url()

params = Map.put(params, "wallpaper_url", socket.assigns.wallpaper_url)
IO.inspect(params)

case AshPhoenix.Form.submit(form,
params: params
) do
{:ok, configuration} ->
{:noreply,
socket
|> assign(form: form)
|> assign(configuration: configuration)
|> put_flash(:info, "Settings updated successfully")
|> push_patch(to: ~p"/rosegarden/configuration")}

{:error, form} ->
Logger.error(AshPhoenix.Form.errors(form, for_path: :all))

{:noreply, assign(socket, form: form)}
end
end

defp maybe_put_uploaded_wallpaper_url(socket) do
wallpaper_url = consume_uploads(socket) |> List.first()

if(wallpaper_url) do
# a wallpaper was uploaded
Logger.info("handling uploaded wallpaper")
assign(socket, :wallpaper_url, wallpaper_url)
else
# pass through form unchanged
socket
end
end
def handle_event("save", %{"form" => params}, socket) do
form = AshPhoenix.Form.validate(socket.assigns.form, params)

socket = socket |> maybe_put_uploaded_wallpaper_url()

params = Map.put(params, "wallpaper_url", socket.assigns.wallpaper_url)
IO.inspect(params)

case AshPhoenix.Form.submit(form,
params: params
) do
{:ok, configuration} ->
{:noreply,
socket
|> assign(form: form)
|> assign(configuration: configuration)
|> put_flash(:info, "Settings updated successfully")
|> push_patch(to: ~p"/rosegarden/configuration")}

{:error, form} ->
Logger.error(AshPhoenix.Form.errors(form, for_path: :all))

{:noreply, assign(socket, form: form)}
end
end

defp maybe_put_uploaded_wallpaper_url(socket) do
wallpaper_url = consume_uploads(socket) |> List.first()

if(wallpaper_url) do
# a wallpaper was uploaded
Logger.info("handling uploaded wallpaper")
assign(socket, :wallpaper_url, wallpaper_url)
else
# pass through form unchanged
socket
end
end
ZachDaniel
ZachDanielโ€ข2y ago
Yeah you probably should assign it to nil on mount That looks pretty reasonable to me ๐Ÿ‘ At some point I'd like to support file uploads natively in AshPhoenix.Form so you can just say which params are file uploads or something along those lines. Would need some workshopping/might not be realistic though
axdc
axdcOPโ€ข2y ago
okay this feels like a breakthrough, excited rn is there anything devastatingly busted about this approach? it's working :>
def handle_event("save", %{"form" => params}, socket) do
form = AshPhoenix.Form.validate(socket.assigns.form, params)

{socket, params} = maybe_put_uploaded_wallpaper_url(socket, params)

IO.inspect(params)

case AshPhoenix.Form.submit(form,
params: params
) do
{:ok, configuration} ->
{:noreply,
socket
|> assign(form: form)
|> assign(configuration: configuration)
|> put_flash(:info, "Settings updated successfully")
|> push_patch(to: ~p"/rosegarden/configuration")}

{:error, form} ->
Logger.error(AshPhoenix.Form.errors(form, for_path: :all))

{:noreply, assign(socket, form: form)}
end
end


defp maybe_put_uploaded_wallpaper_url(socket, params) do
wallpaper_url = consume_uploads(socket) |> List.first()

if(wallpaper_url) do
# a wallpaper was uploaded
Logger.info("handling uploaded wallpaper")
socket = assign(socket, :wallpaper_url, wallpaper_url)
params = Map.put(params, "wallpaper_url", socket.assigns.wallpaper_url)
{socket, params}
else
# pass through unchanged
{socket, params}
end
end
def handle_event("save", %{"form" => params}, socket) do
form = AshPhoenix.Form.validate(socket.assigns.form, params)

{socket, params} = maybe_put_uploaded_wallpaper_url(socket, params)

IO.inspect(params)

case AshPhoenix.Form.submit(form,
params: params
) do
{:ok, configuration} ->
{:noreply,
socket
|> assign(form: form)
|> assign(configuration: configuration)
|> put_flash(:info, "Settings updated successfully")
|> push_patch(to: ~p"/rosegarden/configuration")}

{:error, form} ->
Logger.error(AshPhoenix.Form.errors(form, for_path: :all))

{:noreply, assign(socket, form: form)}
end
end


defp maybe_put_uploaded_wallpaper_url(socket, params) do
wallpaper_url = consume_uploads(socket) |> List.first()

if(wallpaper_url) do
# a wallpaper was uploaded
Logger.info("handling uploaded wallpaper")
socket = assign(socket, :wallpaper_url, wallpaper_url)
params = Map.put(params, "wallpaper_url", socket.assigns.wallpaper_url)
{socket, params}
else
# pass through unchanged
{socket, params}
end
end
still getting used to elixir's scope and mindset of assigning from private functions, instead of the nested if hell i find myself reaching for first. but this is sooo much cleaner ๐Ÿ˜ญ that would be fantastic ๐Ÿ™‚
ZachDaniel
ZachDanielโ€ข2y ago
Some preliminary research tells me that it should be possible but not necessarily easy ๐Ÿ™‚ actually....we could use the form path plus field to make it happen what does your consume_uploads function look like?
axdc
axdcOPโ€ข2y ago
defp consume_uploads(socket) do
consume_uploaded_entries(socket, :wallpaper_url, fn %{path: path}, _entry ->
file = File.read!(path)

object_key = "wallpaper/" <> socket.assigns.current_site.id <> ".jpg"

case ExAws.S3.put_object(
"panacea",
object_key,
file,
content_type: "image/jpeg",
acl: :public_read
)
|> ExAws.request() do
{:ok, _} -> Logger.info("Uploaded file")
{:error, error} -> Logger.error("Error uploading file: #{IO.inspect(error)}")
end

cache_buster = "?" <> (System.os_time() |> Integer.to_string())

url =
"https://panacea.sfo3.cdn.digitaloceanspaces.com/" <>
object_key <> cache_buster

{:ok, url}
end)
end
defp consume_uploads(socket) do
consume_uploaded_entries(socket, :wallpaper_url, fn %{path: path}, _entry ->
file = File.read!(path)

object_key = "wallpaper/" <> socket.assigns.current_site.id <> ".jpg"

case ExAws.S3.put_object(
"panacea",
object_key,
file,
content_type: "image/jpeg",
acl: :public_read
)
|> ExAws.request() do
{:ok, _} -> Logger.info("Uploaded file")
{:error, error} -> Logger.error("Error uploading file: #{IO.inspect(error)}")
end

cache_buster = "?" <> (System.os_time() |> Integer.to_string())

url =
"https://panacea.sfo3.cdn.digitaloceanspaces.com/" <>
object_key <> cache_buster

{:ok, url}
end)
end
ZachDaniel
ZachDanielโ€ข2y ago
Yeah so we'd basically just need to get a callback function that gets a path/entry Sounds interesting. What I'd really like to do is make a file uploading extension for Ash that each API type can use to do certain things i.e in graphql it would add something like a "presigned_url" action and in this it would say "hey, gimme a handler for the file", or maybe just do it magically with the extension

Did you find this page helpful?