Splitting a stream of posts into seperate column components

Hi all,

I am very new to Elixir and Phoenix, so forgive me if this is an obvious question…

I am trying to create a grid layout displaying a stream of posts from a SQL database. In order to get the display right, I need to split these posts up into different row <div>s. It’s easy to get a simple list of posts working, but when I split the stream up into multiple lists like this I can’t get it to update correctly. It loads correctly on mount, but generating a new post wipes out all the others and displays only one. What is the correct way to go about something like this?

For reference, here is a picture of the display I’m going for: https://i.imgur.com/JUGPuRj.png

And here is my code:

defmodule DinoWeb.PostLive.Index do
  use DinoWeb, :live_view

  alias Dino.Posts
  alias Dino.Posts.Post

  @impl true
  def mount(_params, _session, socket) do
    {:ok, stream(socket, :posts, Posts.list_posts())}
  end

  @impl true
  def handle_params(params, _url, socket) do
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end

  defp apply_action(socket, :edit, %{"id" => id}) do
    socket
    |> assign(:page_title, "Edit Post")
    |> assign(:post, Posts.get_post!(id))
  end

  defp apply_action(socket, :new, _params) do
    socket
    |> assign(:page_title, "New Post")
    |> assign(:post, %Post{})
  end

  defp apply_action(socket, :index, _params) do
    socket
    |> assign(:page_title, "Listing Posts")
    |> assign(:post, nil)
  end

  @impl true
  def handle_info({DinoWeb.PostLive.FormComponent, {:saved, post}}, socket) do
    {:noreply, stream_insert(socket, :posts, post)}
  end

  @impl true
  def handle_event("delete", %{"id" => id}, socket) do
    post = Posts.get_post!(id)
    {:ok, _} = Posts.delete_post(post)

    {:noreply, stream_delete(socket, :posts, post)}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <.header>
      Listing Posts
      <:actions>
        <.link patch={~p"/posts/new"}>
          <.button>New Post</.button>
        </.link>
      </:actions>
    </.header>

    <.image_grid id="posts" phx-update="stream" posts={@streams.posts} />

    <.modal :if={@live_action in [:new, :edit]} id="post-modal" show on_cancel={JS.patch(~p"/posts")}>
      <.live_component
        module={DinoWeb.PostLive.FormComponent}
        id={@post.id || :new}
        title={@page_title}
        action={@live_action}
        post={@post}
        patch={~p"/posts"}
      />
    </.modal>
    """
  end

  def image_grid(assigns) do
    assigns = assign(assigns, :cols, split_list(assigns.posts, 4))

    ~H"""
    <div class="grid grid-cols-2 md:grid-cols-4 gap-1">
      <.image_col :for={col <- @cols} posts={col} />
    </div>
    """
  end

  def image_col(assigns) do
    ~H"""
    <div>
      <.image_card :for={{post_id, post} <- @posts} id={post_id} image_link={post.image_link} />
    </div>
    """
  end

  def image_card(assigns) do
    ~H"""
    <div class="my-1 mx-auto">
      <a href="#">
        <img
          class="object-cover w-full overflow-hidden rounded-md"
          style="max-height: 512px;"
          src={"#{@image_link}"}
        />
      </a>
    </div>
    """
  end

  defp split_list(list, n) do
    chunk_size = div(Enum.count(list) + n - 1, n)

    case(chunk_size) do
      0 -> []
      _ -> Enum.chunk_every(list, chunk_size)
    end
  end
end

Review the docs on Required DOM attributes:
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#stream/4-required-dom-attributes

You are missing an ID on the phx-update stream container, as well as ID’s on the immediate children on the container for each stream item. When you enumerate the stream, you get each child DOM id passed as at the first element in the tuple along with the stream item. Hope that helps!