Core_components modal does not work with state

Hi folks, I installed phoenix "~> 1.7.0-rc.0" and Elixir 1.14.2 and my LiveView version is 0.18.3; after that I test the modal of core_components like this:

    <.modal id="delete_confirm" show={true}>
      Are you sure you?
      <:confirm>OK</:confirm>
      <:cancel>Cancel</:cancel>
    </.modal>

it works, and I can see the modal as well as possible, but when I replaced the Boolean value with a state like this:

def mount(_params, _, socket) do
    new_socket = assign(socket, modal_status: false, section_id: nil)
    {:ok, new_socket}
end

def render(assigns) do
    ~H"""
    <.modal id="delete_confirm" show={@modal_status}>
      Are you sure you?
      <:confirm>OK</:confirm>
      <:cancel>Cancel</:cancel>
    </.modal>
    <p phx-click="delete_access" phx-value-id="11" phx-value-type="section-drag">open modal</p>
    """
end

def handle_event("delete_access", %{"id" => id, "type" => "section-drag"}, socket) do
    IO.inspect(id)
    new_socket = assign(socket, modal_status: true, section_id: id)
    # Show modal and get access from user
    {:noreply, new_socket}
end

Unfortunately, my modal does not work and nothing happened

Is there any problem in my code?
Thank you


Update

I think it is because this phx-mounted, it does not let me use initial page render, am I right?

<div id={@id} phx-mounted={@show && show_modal(@id)} class="relative z-50 hidden">

I can use it like this:

def render(assigns) do
    ~H"""
    <%= if @modal_status do %>
    <.modal id="delete_confirm" show={true}>
      Are you sure you?
      <:confirm>OK</:confirm>
      <:cancel>Cancel</:cancel>
    </.modal>
    <% end %>
    <p phx-click="delete_access" phx-value-id="11" phx-value-type="section-drag">open modal</p>
    """
  end

But why LiveView Core_components uses mounted here?

The show property is meant to be used when you must open the modal when the page loads. For instance, if you have a route that leads to a modal open when the navigation happens. That’s why it uses phx-mounted and runs only once.

You actually don’t need to keep the modal state in your LiveView or LiveComponent, since the modal HTML will be there all you need to do is use the show_modal("id") JS command and use the on_confirm to send the event.

Based on your code, you can have something like this:

def mount(_params, _, socket) do
  {:ok, socket}
end

def render(assigns) do
  ~H"""
  <.modal id="delete_confirm"
    on_confirm={
      JS.push("delete_access", value: %{id: "11", type: "section-drag"}) # <--- Send the event when you click OK
      |> hide_modal("delete-confirm") # <--- You don't need this if you redirect from your event
    }
  >
    Are you sure?
    <:confirm>OK</:confirm>
    <:cancel>Cancel</:cancel>
  </.modal>
  <p phx-click={show_modal("delete_confirm")}>open modal</p> # <--- Open the modal
  """
end

def handle_event("delete_access", %{"id" => id, "type" => "section-drag"}, socket) do
  # Do the deletion
  {:noreply, socket}
end

(I’ve added some comments so the code is not valid)

1 Like

@shamanime Hi again how can start the modal programmatically like in handle_event? Is there a way not to use state like the first if I put on top post?

Thank you

There’s a few scenarios I can imagine where that would be useful and there’s a couple of ways to do it.

If you know the modal will always open on the event, I’d still use the phx-click that triggers the event to show the modal and push the event:

<p phx-click={JS.push("event_name", value: %{id: 1234}) |> show_modal("modal_id")}>open modal and send event</p>

You need to consider why/if keeping the modal in a state is necessary, in the example above it is not.
But if it is, for instance, if an event may or may not display the modal, updating the state and using the if as you did works just fine (because as you know changing the show attr won’t work after the mount).

Perhaps another approach that would work depending on what you’re trying to accomplish is using push_event/3.

1 Like

Hi @shamanime, unfortunately I could not be able to find how to create an element in JavaScript side and put this JS.push("event_name", value: %{id: 1234}) |> show_modal("modal_id") on it.

For example, I create phx-click in JavaScript like this:

htmlElement.innerHTML = `
    <svg
      xmlns="http://www.w3.org/2000/svg"
      fill="none"
      viewBox="0 0 24 24"
      stroke-width="1.5"
      stroke="currentColor"
      class="w-6 h-6 text-red-600 cursor-pointer"
      phx-click="delete"
      phx-value-type="access"
      phx-value-id="${htmlElement.id}"
    >
      <path
        stroke-linecap="round"
        stroke-linejoin="round"
        d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
      />
    </svg>
`;

If an element exists in a page it works perfectly, but in my program I create this element with dom in js side


2 ways I can think about them, the first one is condition like my top post and second one create a custom modal and push_event in my handle_event if is not there any way to active show_modal in handle_event :thinking: :thinking: :thinking:


Update:

I did this way and it worked, but is safe?:

phx-click=${"[[&quot;push&quot;,{&quot;event&quot;:&quot;delete&quot;,&quot;value&quot;:{&quot;id&quot;:1234}}],[&quot;show&quot;,{&quot;display&quot;:null,&quot;time&quot;:200,&quot;to&quot;:&quot;#delete_confirm&quot;,&quot;transition&quot;:[[],[],[]]}],[&quot;show&quot;,{&quot;display&quot;:null,&quot;time&quot;:200,&quot;to&quot;:&quot;#delete_confirm-bg&quot;,&quot;transition&quot;:[[&quot;transition-all&quot;,&quot;transform&quot;,&quot;ease-out&quot;,&quot;duration-300&quot;],[&quot;opacity-0&quot;],[&quot;opacity-100&quot;]]}],[&quot;show&quot;,{&quot;display&quot;:null,&quot;time&quot;:200,&quot;to&quot;:&quot;#delete_confirm-container&quot;,&quot;transition&quot;:[[&quot;transition-all&quot;,&quot;transform&quot;,&quot;ease-out&quot;,&quot;duration-300&quot;],[&quot;opacity-0&quot;,&quot;translate-y-4&quot;,&quot;sm:translate-y-0&quot;,&quot;sm:scale-95&quot;],[&quot;opacity-100&quot;,&quot;translate-y-0&quot;,&quot;sm:scale-100&quot;]]}],[&quot;focus_first&quot;,{&quot;to&quot;:&quot;#delete_confirm-content&quot;}]]"}

Thank you

I’m not sure, I have never used something like that.

From what I understood this may be related to what you’re trying to do with Sortable and I’m wondering if you could just use Sortable to dispatch/receive messages to your LV and you use the LV to control the HTML elements? Instead of letting Sortable clone/modify the DOM, you update the LV state that renders the new element and then you wouldn’t have this issue.

1 Like

Hi again,

I compared the both server-side created list and the JSON live view client sends

For example:

  def handle_event("delete", %{"id" => id}, socket) do
    JS.push("delete", value: %{id: id, type: "dom"})
    |> hide_modal("delete_confirm")
    |> Map.get(:ops)
    |> Jason.encode!()
    |> IO.inspect()

    {:noreply, socket}
  end

The output, you can use it in your JSON to send or push event:

"[[\"push\",{\"event\":\"delete\",\"value\":{\"id\":\"section-drag-clone-51d25b15-fa3a-4326-91aa-ec70e34aab6d\",\"type\":\"dom\"}}],[\"hide\",{\"time\":200,\"to\":\"#delete_confirm-bg\",\"transition\":[[\"transition-all\",\"transform\",\"ease-in\",\"duration-200\"],[\"opacity-100\"],[\"opacity-0\"]]}],[\"hide\",{\"time\":200,\"to\":\"#delete_confirm-container\",\"transition\":[[\"transition-all\",\"transform\",\"ease-in\",\"duration-200\"],[\"opacity-100\",\"translate-y-0\",\"sm:scale-100\"],[\"opacity-0\",\"translate-y-4\",\"sm:translate-y-0\",\"sm:scale-95\"]]}],[\"hide\",{\"time\":200,\"to\":\"#delete_confirm\",\"transition\":[[\"block\"],[\"block\"],[\"hidden\"]]}],[\"pop_focus\",{}]]"

Unfortunately, I think I can not do this, because sortable JS pass some parameters and it has some method to manage a dragged and dropped items.
But for now it works and all the process I am doing is for admin a website, not their users.

this is the JSON, modal sends to server in our browser:

in Elixir heex file:

<.modal
  id="delete_confirm"
  on_confirm={
    JS.push("delete", value: %{id: @section_id, type: "dom"})
    |> hide_modal("delete_confirm")
  }
>
  Are you sure?
  <:confirm>OK</:confirm>
  <:cancel>Cancel</:cancel>
</.modal>

I’m sorry I was not more clear, I figured how you got to that encoded string and what I meant was something like “well that doesn’t sound easy to understand/maintain so I think there must be a cleaner way of doing it”.

Of course you said it does work, and anything working is better than something perfect that doesn’t. :+1:

1 Like