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 (+)

After click (+)

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 
<%= 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.
-
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.
-
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