In my live view app, I have a form where user clicks a button that shows a modal dialog previously hidden. From the dialog, user can select a file for upload. When upload is done, dialog is hidden automatically by sending even to the browser via Phoenix.LiveView.push_event/3
. if user types into a form element after this, the dialog will be come visible again.
However, if dialog is hidden via Phoenix.LiveView.JS.hide/2
I never get this behavior.
Here is the code that powers the component:
defmodule MyAppWeb.UploadComponent do
use MyAppWeb, :live_component
alias Phoenix.LiveView.JS
alias Ecto.UUID
alias Ecto.Multi
alias MyApp.Repo
alias MyAppWeb.Endpoint
alias MyApp.Uploads.Upload
@impl true
def update(assigns, socket) do
changeset = Upload.changeset()
{
:ok,
socket
|> allow_upload(
:file,
max_file_size: Endpoint.max_file_size(),
# accept: assigns.accept,
accept: :any,
max_entries: 1
# external: &presign_entry/2
)
|> assign(assigns)
|> assign(:changeset, changeset)
|> assign(:show_submit, false)
}
end
@impl true
def handle_event("validate", _params, socket) do
{
:noreply,
socket
|> assign(:show_submit, true)
}
end
@impl true
def handle_event("cancel-upload", _, socket) do
socket =
case socket.assigns.uploads.file.entries do
[] ->
socket
[entry] ->
# Call to cancel_upload/3 while upload is in flight will cause
# handle_event("save", _, socket) to be called with
# socket.assigns.file.entries = [] when cancel_upload/3 settles. It
# is in handle_event/3 that we will dismiss the dialog.
socket
|> cancel_upload(:file, entry.ref)
end
{
:noreply,
socket
|> assign(:show_submit, false)
}
end
def handle_event("save", _params, socket) do
uploaded_files =
consume_uploaded_entries(socket, :file, fn %{path: path}, entry ->
file_id = UUID.generate()
file_map = %{
filename: entry.client_name,
path: path
}
multi =
Multi.new()
|> Multi.insert(
file_id,
Upload.changeset(%Upload{id: file_id}, %{file: file_map})
)
{:ok, multi}
end)
|> Enum.reduce(Multi.new(), &Multi.merge(&2, fn _ -> &1 end))
|> Repo.transaction()
|> case do
{:ok, entries} ->
Map.values(entries)
{:error, _operation_name, errors} ->
dbg(errors)
[]
end
case uploaded_files do
[] ->
:ok
_ ->
{parent_module, parent_id} = socket.assigns.send_reesult_to
send_update(
parent_module,
id: parent_id,
uploaded_files: uploaded_files
)
end
{
:noreply,
socket
|> assign(:show_submit, false)
|> push_event(
"dismiss-add-file-dialog",
%{
id: "add-file-dialog",
# When cancel_upload/3 completes, we get a re-render which has the
# potential to show the dialog. We must therefore delay hiding dialog
# until cancel_upload/3 has settled. `debounce` flag tells client
# browser to hide dialog after a timeout.
debounce: true
}
)
}
end
# defp error_to_string(:too_large) do
# "Too large"
# end
#
# defp error_to_string(:too_many_files) do
# "You have selected too many files"
# end
#
# defp error_to_string(:not_accepted) do
# "You have selected an unacceptable file type"
# end
defp hide_self(js \\ %JS{}) do
js
|> JS.push("cancel-upload")
|> JS.hide(
to: "#add-file-dialog",
transition: "fade-out"
)
end
@impl true
def render(assigns) do
~H"""
<form
class={[
"modal",
"fade-in"
]}
tabindex="-1"
id="add-file-dialog"
phx-submit="save"
phx-change="validate"
phx-target={@myself}
style={~s( display:none )}
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="addFileLabel">
Select File
</h1>
<button
type="button"
class="btn-close"
aria-label="Close"
phx-click={hide_self()}
phx-target={@myself}
>
</button>
</div>
<div class="modal-body">
<%= for err <- upload_errors(@uploads.file) do %>
<p class="notification is-danger max-w-xs ">
<%= Phoenix.Naming.humanize(err) %>
</p>
<% end %>
<%= for entry <- @uploads.file.entries do %>
<div class={[
"progress",
"mb-1",
!entry.preflighted? && "d-none"
]}>
<div
class={[
"progress-bar",
"progress-bar-striped",
"progress-bar-animated",
"bg-secondary"
]}
role="progressbar"
aria-label="Progressbar for file upload"
aria-valuenow={entry.progress}
aria-valuemin="0"
aria-valuemax="100"
style={~s(width: #{entry.progress}%)}
>
</div>
</div>
<% end %>
<.live_file_input
class={[
"form-control"
]}
upload={@uploads.file}
/>
</div>
<div class={[
"modal-footer"
]}>
<button
type="button"
class={[
"btn",
"btn-secondary"
]}
phx-target={@myself}
phx-click={hide_self()}
>
Close
</button>
<button
class={[
"btn",
"btn-info"
]}
style={
~s(display: #{if @show_submit, do: "inline-block", else: "none"})
}
id="add-file-dialog-save"
type="submit"
phx-disable-with="Uploading, please wait..."
phx-target={@myself}
>
Save changes
</button>
</div>
</div>
</div>
</form>
"""
end
end
And the javascript code that responds to Phoenix.LiveView.push_event/3
:
window.addEventListener("phx:dismiss-add-file-dialog", (evt) => {
const {
detail: { id, debounce },
} = evt;
const el = document.getElementById(id);
if (el) {
setTimeout(
() => {
el.style.display = "none";
},
debounce ? LATENCY_MILI_SEC + 5 : 0
);
}
});