Live view renders all html in "for" loop not only part of it

Hi I try to understand LiveView . I want to expand and hide HTML after clicking button.
Here code :

use Phoenix.LiveView

  @packs [ 
    %{name: "PACK1", slug: "pack1"} , 
    %{name: "PACK2", slug: "pack2"}
  ]

  def mount(_params, _session , socket) do  
         {:ok, assign(socket,show_pack: false , pack_slug: nil, packs: @packs) }   
  end 
  def handle_event("open_pack", %{"pack_slug" => pack_slug}, socket) do  
    {:noreply, assign(socket,show_pack: true, pack_slug: pack_slug)} 
  end 
  def handle_event("close_pack", %{"pack_slug" => pack_slug} , socket) do  
    {:noreply, assign(socket,show_pack: false, pack_slug: pack_slug) } 
  end 


  def render(assigns) do  
    ~L"""
 <div class="container">
   <%= for pack <- @packs do %> 
     <div class="row"> 
        <div class="column"><b> <%= pack[:name] %> </b> 
           <%= if  pack[:slug] == @pack_slug || @pack_slug == nil do %>  
              <%= if @show_pack == true do %> 
                   <button phx-click="close_pack" phx-value-pack_slug="<%= pack[:slug] %>" > - </button> 
                   <div class="row"> </br>   
                      <div class="column column-offset-10"><b> <%= pack[:name] %> is open </b> </div>          
                    </div> 
              <% else %> 
         
                   <button phx-click="open_pack" phx-value-pack_slug="<%= pack[:slug] %>" > + </button> 
               <% end %> 
           <% end %>   
        </div>
    </div> 
  <% end %>  
</div> 

I want to open and close many packs but keep state if pack is open or closed.
I go through “for” loop to list all packs as initial state (packs are closed) . But when I click (+) button of the first pack , LiveView goes again through whole “for” loop and hide (+) button of the second pack, forever.
I think it should modify only part of code responsible for the first pack but result is different.

Before click (+)
BeforeClick
After click (+)
AfterClick

I want to modify part of HTML responsible for certain pack independently, but all code is modified.

Thanks for tip.
Maybe should I use live component ?

After you expand a pack, you won’t enter this if anymore:

<%= if  pack[:slug] == @pack_slug || @pack_slug == nil do %>

You can duplicate the inner else statement on the outer if branch as well to get this working at first and then make it more beautifully :smiley:

<%= if  pack[:slug] == @pack_slug || @pack_slug == nil do %>
    <%= if @show_pack == true do %>
      <button phx-click="close_pack" phx-value-pack_slug="<%= pack[:slug] %>" > - </button>
      <div class="row"> </br>
        <div class="column column-offset-10"><b> <%= pack[:name] %> is open </b> </div>
      </div>
    <% else %>
      <button phx-click="open_pack" phx-value-pack_slug="<%= pack[:slug] %>" > + </button>
    <% end %>
+ <% else %>
+  <button phx-click="open_pack" phx-value-pack_slug="<%= pack[:slug] %>" > + </button>
<% end %>

Hi , thanks for tip but it isn’t a solution . When pack1 is open then pack2 is closed and vice versa. When I open pack2 then pack1 is closed.I can’t keep state of pack independently. Live view still goes through whole code .
I can’t figure out how to transform only part of code after “for” loop initialization.

I thought that’s what you wanted to implement, similar to a collapse component, where opening one would close all the others.

  1. You could extend your data structure with one more key show and then on open/close, you would simply change the value to true or false for that specific entry.

  2. Create a PackComponent that will keep its own state and handle the open/close actions.

Here’s the code for the 1st option:

  @packs [
    %{name: "PACK1", slug: "pack1", show: false},
    %{name: "PACK2", slug: "pack2", show: false},
    %{name: "PACK3", slug: "pack3", show: false},
    %{name: "PACK4", slug: "pack4", show: false}
  ]

  def mount(_params, _session, socket) do
    {:ok, assign(socket, packs: @packs)}
  end

  def render(assigns) do
    ~L"""
     <div class="container">
       <%= for pack <- @packs do %>
         <div class="row">
            <div class="column"><b> <%= pack[:name] %> </b>
              <%= if  pack[:show] do %>
                  <button phx-click="toggle" phx-value-pack_slug="<%= pack[:slug] %>" > - </button>
                  <div class="row"> </br>
                      <div class="column column-offset-10"><b> <%= pack[:name] %> is open </b> </div>
                  </div>
              <% else %>
                    <button phx-click="toggle" phx-value-pack_slug="<%= pack[:slug] %>" > + </button>
              <% end %>
            </div>
        </div>
      <% end %>
    </div>
    """
  end

  def handle_event("toggle", %{"pack_slug" => pack_slug}, socket) do
    socket =
      socket
      |> update(:packs, fn packs ->
        Enum.map(packs, fn
          %{slug: ^pack_slug} = pack -> Map.update!(pack, :show, fn state -> !state end)
          pack -> pack
        end)
      end)

    {:noreply, socket}
  end

Obviously, whenever anything changes inside packs, LiveView will automatically re-render the entire list. An improvement to this implementation would be going the stateful LiveComponent way.

1 Like

Thanks a lot. Now I better understand topic. I try also do this by LiveComponent due to huge list in final implementation of code.

1 Like